-
# frozen_string_literal: true
-
1
require 'timeout'
-
1
require_relative 'utf8_clean'
-
-
1
module Capture3WithTimeout
-
-
1
def capture3_with_timeout(context, command, spawn_opts)
-
# Based on https://gist.github.com/pasela/9392115
-
opts = {
-
stdin_data: spawn_opts.delete(:stdin_data) || '',
-
binmode: spawn_opts.delete(:binmode) || false,
-
timeout: spawn_opts.delete(:timeout),
-
signal: spawn_opts.delete(:signal) || :TERM,
-
kill_after: spawn_opts.delete(:kill_after),
-
}
-
-
in_r, in_w = IO.pipe
-
out_r, out_w = IO.pipe
-
err_r, err_w = IO.pipe
-
in_w.sync = true
-
-
if opts[:binmode]
-
in_w.binmode
-
out_r.binmode
-
err_r.binmode
-
end
-
-
spawn_opts[:in] = in_r
-
spawn_opts[:out] = out_w
-
spawn_opts[:err] = err_w
-
-
result = { pid:nil, stdout:nil, stderr:nil, status:nil }
-
-
out_reader = nil
-
err_reader = nil
-
wait_thr = nil
-
-
process = context.process
-
threader = context.threader
-
begin
-
result[:timed_out] = false
-
Timeout.timeout(opts[:timeout]) do
-
result[:pid] = process.spawn(command, spawn_opts)
-
wait_thr = process.detach(result[:pid])
-
in_r.close
-
out_w.close
-
err_w.close
-
-
out_reader = threader.thread { out_r.read }
-
err_reader = threader.thread { err_r.read }
-
-
in_w.write(opts[:stdin_data])
-
in_w.close
-
-
result[:status] = wait_thr.value
-
end
-
rescue Timeout::Error
-
result[:timed_out] = true
-
pid = spawn_opts[:pgroup] ? -result[:pid] : result[:pid]
-
process.kill(opts[:signal], pid)
-
if opts[:kill_after]
-
unless wait_thr.join(opts[:kill_after])
-
process.kill(:KILL, pid)
-
end
-
end
-
yield if block_given?
-
ensure
-
result[:status] = wait_thr.value if wait_thr
-
result[:stdout] = out_reader.value if out_reader
-
result[:stderr] = err_reader.value if err_reader
-
out_r.close unless out_r.closed?
-
err_r.close unless err_r.closed?
-
end
-
-
result.delete(:pid)
-
-
result
-
end
-
-
end
-
# frozen_string_literal: true
-
1
require_relative 'externals/stdout_logger'
-
1
require_relative 'externals/process_adapter'
-
1
require_relative 'externals/bash_sheller'
-
1
require_relative 'externals/asynchronous_threader'
-
1
require_relative 'node'
-
1
require_relative 'prober'
-
1
require_relative 'puller'
-
1
require_relative 'runner'
-
-
1
class Context
-
-
1
def initialize(options = {})
-
60
@node = options[:node] || Node.new(self)
-
60
@prober = options[:prober] || Prober.new(self)
-
60
@puller = options[:puller] || Puller.new(self)
-
60
@runner = options[:runner] || Runner.new(self)
-
-
60
@logger = options[:logger] || StdoutLogger.new
-
60
@process = options[:process] || ProcessAdapter.new
-
60
@sheller = options[:sheller] || BashSheller.new(self)
-
60
@threader = options[:threader] || AsynchronousThreader.new
-
end
-
-
1
attr_reader :node, :prober, :puller, :runner
-
1
attr_reader :logger, :process, :sheller, :threader
-
-
end
-
# frozen_string_literal: true
-
-
1
module Empty
-
-
1
def self.binding
-
super
-
end
-
-
end
-
# frozen_string_literal: true
-
-
1
class AsynchronousThreader
-
-
1
def thread(&block)
-
Thread.new(&block)
-
end
-
-
end
-
# frozen_string_literal: true
-
1
require 'open3'
-
-
1
class BashSheller
-
-
1
def initialize(context)
-
60
@context = context
-
end
-
-
1
def capture(command)
-
3
stdout,stderr,r = Open3.capture3(command)
-
2
status = r.exitstatus
-
2
unless status === 0
-
message = [
-
1
"command:#{command}:",
-
"stdout:#{stdout}:",
-
"stderr:#{stderr}:",
-
"status:#{status}:"
-
].join("\n")
-
1
logger.log(message)
-
end
-
2
[ stdout, stderr, status ]
-
end
-
-
1
private
-
-
1
def logger
-
1
@context.logger
-
end
-
-
end
-
# frozen_string_literal: true
-
-
1
class ProcessAdapter
-
-
1
def detach(pid)
-
Process.detach(pid)
-
end
-
-
1
def kill(signal, pid)
-
Process.kill(signal, pid)
-
end
-
-
1
def spawn(command, options)
-
Process.spawn(command, options)
-
end
-
-
end
-
# frozen_string_literal: true
-
-
1
class StdoutLogger
-
-
1
def initialize
-
3
@stream = $stdout
-
end
-
-
1
def log(message)
-
3
unless message.empty?
-
2
message += "\n" if message[-1] != "\n"
-
2
@stream.write(message)
-
end
-
end
-
-
end
-
# frozen_string_literal: true
-
1
module FilesDelta
-
-
# files_in (old)
-
# Format == { "hiker.c" => "#include..." }
-
# files_out (new)
-
# Format == { "hiker.c" => { "content": "#include...", truncated: false } }
-
-
1
def files_delta(old, new)
-
5
changed = {}
-
5
deleted = {}
-
5
old.each do |filename, content|
-
3
if !new.has_key?(filename)
-
1
deleted[filename] = { 'content' => content }
-
2
elsif new[filename]['content'] != content
-
1
changed[filename] = new[filename]
-
end
-
3
new.delete(filename) # same (destructive)
-
end
-
5
[ created=new, deleted, changed ]
-
end
-
-
# The old files are assumed to NOT be truncated.
-
# To check for a changed file we only have to check the
-
# new files' content. If the new file has been truncated
-
# then the content must have changed since the old files
-
# are assumed NON truncated.
-
-
end
-
1
require 'stringio'
-
1
require 'zlib'
-
-
1
module Gnu
-
-
1
def self.unzip(s)
-
4
reader = Zlib::GzipReader.new(StringIO.new(s))
-
4
unzipped = reader.read
-
4
reader.close
-
4
unzipped
-
end
-
-
end
-
1
require 'stringio'
-
1
require 'zlib'
-
-
1
module Gnu
-
-
1
def self.zip(s)
-
4
zipped = StringIO.new('')
-
4
writer = Zlib::GzipWriter.new(zipped)
-
4
writer.write(s)
-
4
writer.close
-
4
zipped.string
-
end
-
-
end
-
# frozen_string_literal: true
-
1
require 'json'
-
1
require 'uri'
-
-
1
module HttpProxy
-
1
class JsonRequester
-
-
1
def initialize(http, hostname, port)
-
3
@http = http
-
3
@hostname = hostname
-
3
@port = port
-
end
-
-
1
def get(path, args)
-
3
request(path, args) do |uri|
-
3
@http.get(uri)
-
end
-
end
-
-
1
def post(path, args)
-
request(path, args) do |uri|
-
@http.post(uri)
-
end
-
end
-
-
1
private
-
-
1
def request(path, args)
-
3
uri = URI.parse("http://#{@hostname}:#{@port}/#{path}")
-
3
req = yield uri
-
3
req.content_type = 'application/json'
-
3
req.body = JSON.fast_generate(args)
-
3
@http.start(@hostname, @port, req)
-
end
-
-
end
-
end
-
# frozen_string_literal: true
-
1
require 'json'
-
-
1
module HttpProxy
-
1
class JsonResponder
-
-
1
def initialize(requester, exception_class)
-
3
@requester = requester
-
3
@exception_class = exception_class
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - -
-
-
1
def get(path, args)
-
3
response = @requester.get(path, args)
-
3
unpacked(response.body, path.to_s)
-
rescue => error
-
fail @exception_class, error.message
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - -
-
-
1
def post(path, args)
-
response = @requester.post(path, args)
-
unpacked(response.body, path.to_s)
-
rescue => error
-
fail @exception_class, error.message
-
end
-
-
1
private
-
-
1
def unpacked(body, path)
-
3
json = json_parse(body)
-
3
unless json.is_a?(Hash)
-
fail error_msg(body, 'is not JSON Hash')
-
end
-
3
if json.has_key?('exception')
-
fail JSON.pretty_generate(json['exception'])
-
end
-
3
unless json.has_key?(path)
-
fail error_msg(body, "has no key for '#{path}'")
-
end
-
3
json[path]
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - -
-
-
1
def json_parse(body)
-
3
if body === ''
-
{}
-
else
-
3
JSON.parse!(body)
-
end
-
rescue JSON::ParserError
-
fail error_msg(body, 'is not JSON')
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - -
-
-
1
def error_msg(body, text)
-
"http response.body #{text}:#{body}"
-
end
-
-
end
-
end
-
# frozen_string_literal: true
-
1
require_relative 'json_requester'
-
1
require_relative 'json_responder'
-
1
require_relative 'net_http_adapter'
-
-
1
module HttpProxy
-
1
class LanguagesStartPoints
-
-
1
class Error < RuntimeError
-
1
def initialize(message)
-
super
-
end
-
end
-
-
# - - - - - - - - - - - - - - - - - - -
-
-
1
def initialize
-
3
adapter = ::HttpProxy::NetHttpAdapter.new
-
3
hostname = 'languages-start-points'
-
3
port = 4524
-
3
requester = ::HttpProxy::JsonRequester.new(adapter, hostname, port)
-
3
@http = ::HttpProxy::JsonResponder.new(requester, Error)
-
end
-
-
# - - - - - - - - - - - - - - - - - - -
-
-
1
def alive?
-
1
@http.get(__method__, {})
-
end
-
-
1
def ready?
-
1
@http.get(__method__, {})
-
end
-
-
1
def sha
-
1
@http.get(__method__, {})
-
end
-
-
# - - - - - - - - - - - - - - - - - - -
-
-
1
def manifest(name)
-
@http.get(__method__, {
-
name:name
-
})
-
end
-
-
end
-
end
-
# frozen_string_literal: true
-
1
require 'net/http'
-
-
1
module HttpProxy
-
1
class NetHttpAdapter
-
-
1
def get(uri)
-
3
Net::HTTP::Get.new(uri)
-
end
-
-
1
def post(uri)
-
Net::HTTP::Post.new(uri)
-
end
-
-
1
def start(hostname, port, req)
-
3
Net::HTTP.start(hostname, port) do |http|
-
3
http.request(req)
-
end
-
end
-
-
end
-
end
-
# frozen_string_literal: true
-
-
1
class Node
-
-
1
def initialize(context)
-
60
@context = context
-
end
-
-
1
def image_names
-
4
command = "docker image ls --format '{{.Repository}}:{{.Tag}}'"
-
4
ls,stderr,status = sheller.capture(command)
-
4
unless status === 0
-
1
raise RuntimeError, stderr
-
end
-
3
ls.split("\n").sort.uniq - ['<none>:<none>']
-
end
-
-
1
private
-
-
1
def sheller
-
4
@context.sheller
-
end
-
-
end
-
# frozen_string_literal: true
-
-
1
class Prober
-
-
1
def initialize(_context)
-
end
-
-
1
def alive?
-
1
true
-
end
-
-
1
def ready?
-
1
true
-
end
-
-
1
def sha
-
1
ENV['SHA']
-
end
-
-
end
-
# frozen_string_literal: true
-
1
require_relative 'synchronized_set'
-
1
require_relative 'tagged_image_name'
-
-
1
class Puller
-
-
1
def initialize(context)
-
60
@context = context
-
60
@pulled = SynchronizedSet.new
-
60
@pulling = SynchronizedSet.new
-
end
-
-
# - - - - - - - - - - - - - - - - - - -
-
-
1
def image_names
-
1
@pulled.to_a
-
end
-
-
# - - - - - - - - - - - - - - - - - - -
-
-
1
def add(image_name)
-
6
@pulled.add(image_name)
-
end
-
-
# - - - - - - - - - - - - - - - - - - -
-
-
1
def pull_image(id:, image_name:)
-
image_name = ::Docker::tagged_image_name(image_name)
-
if !@pulled.include?(image_name)
-
if @pulling.add?(image_name)
-
threader.thread do
-
threaded_pull_image(id, image_name)
-
end
-
end
-
:pulling
-
else
-
:pulled
-
end
-
end
-
-
1
private
-
-
1
def threaded_pull_image(id, image_name)
-
t0 = Time.now
-
command = "docker pull #{image_name}"
-
_,_,status = sheller.capture(command)
-
if status === 0
-
add(image_name)
-
t1 = Time.now
-
took = (t1 - t0).round(1)
-
logger.log("Pulled docker image #{image_name} (#{took} secs)")
-
end
-
ensure
-
@pulling.delete(image_name)
-
end
-
-
# - - - - - - - - - - - - - - - - - - -
-
-
1
def logger
-
@context.logger
-
end
-
-
1
def sheller
-
@context.sheller
-
end
-
-
1
def threader
-
@context.threader
-
end
-
-
end
-
# frozen_string_literal: true
-
1
require 'concurrent'
-
-
1
class RagLambdas
-
-
1
def initialize
-
60
@map = Concurrent::Map.new
-
end
-
-
1
def [](image_name)
-
@map[image_name]
-
end
-
-
1
def compute(image_name, &block)
-
@map.compute(image_name, &block)
-
end
-
-
end
-
# frozen_string_literal: true
-
-
1
module RandomHex
-
-
1
def self.id(size)
-
513
rand(16**(size-1)..16**size).to_s(16)
-
end
-
-
1
HEX_DIGITS = [*('a'..'z'),*('A'..'Z'),*('0'..'9')]
-
-
end
-
# frozen_string_literal: true
-
1
require_relative 'capture3_with_timeout'
-
1
require_relative 'files_delta'
-
1
require_relative 'home_files'
-
1
require_relative 'random_hex'
-
1
require_relative 'sandbox'
-
1
require_relative 'tgz'
-
1
require_relative 'traffic_light'
-
1
require_relative 'utf8_clean'
-
-
1
class Runner
-
-
1
def initialize(context)
-
# Comments marked [X] are expanded at the end of this file.
-
60
@context = context
-
60
@traffic_light = TrafficLight.new(context)
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - -
-
-
1
def run_cyber_dojo_sh(id:, files:, manifest:)
-
image_name = manifest['image_name']
-
if puller.pull_image(id:id, image_name:image_name) != :pulled
-
stdout,stderr,status, created,deleted,changed = dummy_result(141)
-
outcome,log_info = 'pulling', {}
-
else
-
max_seconds = manifest['max_seconds']
-
files_in = Sandbox.in(files)
-
tgz_in = TGZ.of(files_in.merge(home_files(Sandbox::DIR, MAX_FILE_SIZE)))
-
-
result = docker_run_cyber_dojo_sh(id, image_name, max_seconds, tgz_in)
-
-
if result[:timed_out]
-
stdout,stderr,status, created,deleted,changed = dummy_result(142)
-
outcome,log_info = 'timed_out', result
-
log(id:id, image_name:image_name, message:'timed_out', result:utf8_clean(result))
-
elsif result[:status] != 0
-
stdout,stderr,status, created,deleted,changed = dummy_result(143)
-
outcome,log_info = 'faulty',result
-
log(id:id, image_name:image_name, message:'faulty', result:utf8_clean(result))
-
else
-
tgz_out = result[:stdout]
-
stdout,stderr,status, created,deleted,changed = *truncated_untgz(id, image_name, files_in, tgz_out)
-
sss = [ stdout['content'], stderr['content'], status['content'] ]
-
outcome,log_info = *@traffic_light.colour(image_name, *sss)
-
end
-
end
-
-
{
-
'stdout' => stdout,
-
'stderr' => stderr,
-
'status' => status['content'],
-
'outcome' => outcome,
-
'created' => Sandbox.out(created),
-
'deleted' => Sandbox.out(deleted).keys.sort,
-
'changed' => Sandbox.out(changed),
-
'log' => log_info
-
}
-
end
-
-
1
private
-
-
1
include Capture3WithTimeout
-
1
include FilesDelta
-
1
include HomeFiles
-
-
1
KB = 1024
-
1
MB = 1024 * KB
-
1
GB = 1024 * MB
-
-
1
UID = 41966 # [X] sandbox user - runs /sandbox/cyber-dojo.sh
-
1
GID = 51966 # [X] sandbox group - runs /sandbox/cyber-dojo.sh
-
1
MAX_FILE_SIZE = 50 * KB # of stdout, stderr, created, changed
-
-
# - - - - - - - - - - - - - - - - - - - - - -
-
-
1
def dummy_result(n)
-
stdout = truncated('')
-
stderr = truncated('')
-
status = truncated(n.to_s)
-
created,deleted,changed = {},{},{}
-
[ stdout,stderr,status, created,deleted,changed ]
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - -
-
-
1
def docker_run_cyber_dojo_sh(id, image_name, max_seconds, tgz_in)
-
container_name = [ 'cyber_dojo_runner', id, RandomHex.id(8) ].join('_')
-
command = docker_run_cyber_dojo_sh_command(id, image_name, container_name)
-
spawn_opts = {
-
:binmode => true,
-
:kill_after => 1,
-
:pgroup => true,
-
:stdin_data => tgz_in,
-
:timeout => max_seconds
-
}
-
capture3_with_timeout(@context, command, spawn_opts) do
-
# The [docker run] command timed-out.
-
docker_stop_container(id, image_name, container_name)
-
end
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - -
-
-
1
def docker_stop_container(id, image_name, container_name)
-
# If the container is running, stop it.
-
# Note: I have tried using the [docker run] --stop-timeout option
-
# instead of using capture3_with_timeout().
-
# In tests, it fails to stop a container in an infinite loop.
-
command = "docker stop --time 1 #{container_name}"
-
options = { timeout:4 }
-
result = capture3_with_timeout(@context, command, options)
-
unless result[:status] === 0
-
skipped
# :nocov:
-
skipped
log(id:id, image_name:image_name, command:command)
-
skipped
# :nocov:
-
end
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - -
-
-
1
def truncated_untgz(id, image_name, files_in, tgz_out)
-
begin
-
files_out = TGZ.files(tgz_out).each.with_object({}) do |(filename,content),memo|
-
memo[filename] = truncated(content)
-
end
-
stdout = files_out.delete('stdout') || truncated('')
-
stderr = files_out.delete('stderr') || truncated('')
-
status = files_out.delete('status') || truncated('142')
-
created,deleted,changed = files_delta(files_in, files_out)
-
rescue Zlib::GzipFile::Error
-
stdout,stderr,status, created,deleted,changed = dummy_result(144)
-
log(id:id, image_name:image_name, error:'Zlib::GzipFile::Error')
-
end
-
[ stdout,stderr,status, created,deleted,changed ]
-
end
-
-
1
def truncated(raw_content)
-
content = Utf8.clean(raw_content)
-
{
-
'content' => content[0...MAX_FILE_SIZE],
-
'truncated' => content.size > MAX_FILE_SIZE
-
}
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - -
-
-
1
def docker_run_cyber_dojo_sh_command(id, image_name, container_name)
-
# --init makes container removal much faster
-
<<~SHELL.strip
-
docker run \
-
--entrypoint="" \
-
--env CYBER_DOJO_IMAGE_NAME='#{image_name}' \
-
--env CYBER_DOJO_ID='#{id}' \
-
--env CYBER_DOJO_SANDBOX='#{Sandbox::DIR}' \
-
--init \
-
--interactive \
-
--name=#{container_name} \
-
#{TMP_FS_SANDBOX_DIR} \
-
#{TMP_FS_TMP_DIR} \
-
#{ulimits(image_name)} \
-
--rm \
-
--user=#{UID}:#{GID} \
-
#{image_name} \
-
bash -c 'tar -C / -zxf - && bash ~/cyber_dojo_main.sh'
-
SHELL
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - -
-
-
1
def ulimits(image_name)
-
# [1] the nproc --limit is per user across all containers. See
-
# https://docs.docker.com/engine/reference/commandline/run/#set-ulimits-in-container---ulimit
-
# There is no cpu-ulimit. See
-
# https://github.com/cyber-dojo-retired/runner-stateless/issues/2
-
options = [
-
ulimit('core' , 0 ), # no core file
-
ulimit('fsize' , 16*MB), # file size
-
ulimit('locks' , 1024 ), # number of file locks
-
ulimit('nofile', 1024 ), # number of files
-
ulimit('nproc' , 1024 ), # number of processes [1]
-
ulimit('stack' , 16*MB), # stack size
-
'--kernel-memory=768m', # limited
-
'--memory=768m', # max 768MB ram (same swap)
-
'--net=none', # no network
-
'--pids-limit=128', # no fork bombs
-
'--security-opt=no-new-privileges', # no escalation
-
]
-
# Special handling of clang/clang++'s -fsanitize=address
-
if clang?(image_name)
-
options << '--cap-add=SYS_PTRACE'
-
else
-
options << ulimit('data', 4*GB) # data segment size
-
end
-
options.join(SPACE)
-
end
-
-
1
def ulimit(name, limit)
-
"--ulimit #{name}=#{limit}"
-
end
-
-
1
def clang?(image_name)
-
image_name.start_with?('cyberdojofoundation/clang')
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - -
-
# temporary file systems
-
# - - - - - - - - - - - - - - - - - - - - - -
-
# Making the sandbox dir a tmpfs should improve speed.
-
# By default, tmp-fs's are setup as secure mountpoints.
-
# If you use only '--tmpfs #{Sandbox::DIR}'
-
# then a [cat /etc/mtab] will reveal something like
-
# "tmpfs /sandbox tmpfs rw,nosuid,nodev,noexec,relatime,size=10240k 0 0"
-
# o) rw = Mount the filesystem read-write.
-
# o) nosuid = Do not allow set-user-identifier or
-
# set-group-identifier bits to take effect.
-
# o) nodev = Do not interpret character or block special devices.
-
# o) noexec = Do not allow direct execution of any binaries.
-
# o) relatime = Update inode access times relative to modify/change time.
-
# So...
-
# - set exec to make binaries and scripts executable.
-
# - limit size of tmp-fs.
-
# - set ownership.
-
# - - - - - - - - - - - - - - - - - - - - - -
-
-
1
TMP_FS_SANDBOX_DIR = "--tmpfs #{Sandbox::DIR}:exec,size=50M,uid=#{UID},gid=#{GID}"
-
1
TMP_FS_TMP_DIR = '--tmpfs /tmp:exec,size=50M,mode=1777' # Set /tmp sticky-bit
-
-
# - - - - - - - - - - - - - - - - - - - - - -
-
-
1
def utf8_clean(result)
-
result[:stdout] = Utf8.clean(result[:stdout])
-
result[:stderr] = Utf8.clean(result[:stderr])
-
end
-
-
1
def log(properties)
-
@context.logger.log(JSON.pretty_generate(properties))
-
end
-
-
1
def puller
-
@context.puller
-
end
-
-
1
SPACE = ' '
-
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
# [X] Runner's requirements on image_name.
-
# o) sandbox user, uid=41966, gid=51966, home=/home/sandbox
-
# o) bash, file, grep, tar, truncate
-
# These are satisfied by image_name being built with
-
# https://github.com/cyber-dojo-tools/image_builder
-
# https://github.com/cyber-dojo-tools/image_dockerfile_augmenter
-
#
-
# Approval-style test-frameworks compare actual-text against
-
# expected-text and write the actual-text to a file for human
-
# inspection. runner supports this by returning all text files
-
# under /sandbox after cyber-dojo.sh has run.
-
#
-
# Note: The browser's kata/run_tests ajax call timeout is
-
# different to the Runner.run() call timing out.
-
# - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
# frozen_string_literal: true
-
-
1
module Sandbox
-
-
1
DIR = '/sandbox' # where files are saved to in the container
-
-
1
def self.in(arg)
-
# arg { 'hiker.cs' => content }
-
# returns { 'sandbox/hiker.cs' => content }
-
4
if arg.is_a?(Hash)
-
# files
-
2
arg.each.with_object({}) do |(filename,content),memo|
-
2
memo[Sandbox.in(filename)] = content
-
end
-
else
-
# filename: Tar likes relative paths
-
2
unrooted = Sandbox::DIR[1..-1]
-
2
[ unrooted, arg ].join('/')
-
end
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
1
def self.out(arg)
-
# arg { 'sandbox/hiker.cs' => content }
-
# returns { 'hiker.cs' => content }
-
4
if arg.is_a?(Hash)
-
# files
-
2
arg.each.with_object({}) do |(filename,content),memo|
-
2
memo[Sandbox.out(filename)] = content
-
end
-
else
-
# filename
-
2
arg[Sandbox::DIR.size..-1] # same size with / at front or back
-
end
-
end
-
-
end
-
# frozen_string_literal: true
-
1
require 'set'
-
-
1
class SynchronizedSet
-
-
1
def initialize
-
125
@values = Set.new
-
125
@mutex = Mutex.new
-
end
-
-
1
def include?(value)
-
# Documentation for Concurrent::Set suggests it locks
-
# after the include, on each access. This should be faster.
-
6
@mutex.synchronize { @values.include?(value) }
-
end
-
-
1
def add(value)
-
18
@mutex.synchronize { @values.add(value) }
-
end
-
-
1
def add?(value)
-
4
@mutex.synchronize { @values.add?(value) }
-
end
-
-
1
def to_a
-
16
@mutex.synchronize { @values.to_a.sort }
-
end
-
-
1
def delete(value)
-
4
@mutex.synchronize { @values.delete(value) }
-
end
-
-
end
-
# frozen_string_literal: true
-
-
1
module Docker # mix-in
-
-
1
module_function
-
-
1
def tagged_image_name(s)
-
# The image_names harvested from the nodes have an
-
# explicit :latest tag. The image_name in pull_image()
-
# and run_cyber_dojo_sh()'s manifest must match.
-
# eg 'cdf/gcc_assert' ==> 'cdf/gcc_assert:latest'
-
51
i = s.index('/')
-
51
if i.nil? || remote_name?(s[0...i])
-
14
match = s.match(REMOTE_NAME)
-
14
name = match[1]
-
14
tag = match[8]
-
14
digest = match[9]
-
else
-
37
host_name,remote_name = cut(s, i)
-
37
match = remote_name.match(REMOTE_NAME)
-
37
name = "#{host_name}/#{match[1]}"
-
37
tag = match[8]
-
37
digest = match[9]
-
end
-
51
if tag.nil?
-
16
tag = 'latest'
-
end
-
51
"#{name}:#{tag}#{digest}"
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
1
def image_name?(s)
-
74
return false if s.nil?
-
73
return false unless s.is_a?(String)
-
73
i = s.index('/')
-
73
if i.nil? || remote_name?(s[0...i])
-
36
s =~ REMOTE_NAME
-
else
-
37
host_name,remote_name = cut(s, i)
-
37
host_name =~ HOST_NAME && remote_name =~ REMOTE_NAME
-
end
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
1
def cut(s, i)
-
# s = 'cyberdojofoundation/gcc_assert'
-
# i = s.index('/') # 19
-
# s[0..18] == 'cyberdojofoundation'
-
# s[20..-1] == 'gcc_assert'
-
74
[s[0..i-1], s[i+1..-1]]
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
1
def remote_name?(s)
-
90
dns_separator = '.'
-
90
port_separator = ':'
-
90
!s.include?(dns_separator) &&
-
!s.include?(port_separator) &&
-
s != 'localhost'
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
# [[host:port/]registry/]component[:tag][@digest]
-
-
1
CH = 'a-zA-Z0-9'
-
1
COMPONENT = "([#{CH}]|[#{CH}][#{CH}-]*[#{CH}])"
-
1
PORT = '[\d]+'
-
1
HOST_NAME = /^(#{COMPONENT}(\.#{COMPONENT})*)(:(#{PORT}))?$/
-
-
# - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
1
ALPHA_NUMERIC = '[a-z0-9]+'
-
1
SEPARATOR = '([.]{1}|[_]{1,2}|[-]+)'
-
1
REMOTE_COMPONENT = "#{ALPHA_NUMERIC}(#{SEPARATOR}#{ALPHA_NUMERIC})*"
-
1
NAME = "#{REMOTE_COMPONENT}(/#{REMOTE_COMPONENT})*"
-
1
TAG = '[\w][\w.-]{0,127}'
-
1
DIGEST_COMPONENT = '[A-Za-z][A-Za-z0-9]*'
-
1
DIGEST_SEPARATOR = '[-_+.]'
-
1
DIGEST_ALGORITHM = "#{DIGEST_COMPONENT}(#{DIGEST_SEPARATOR}#{DIGEST_COMPONENT})*"
-
1
DIGEST_HEX = "[0-9a-fA-F]{32,}"
-
1
DIGEST = "#{DIGEST_ALGORITHM}[:]#{DIGEST_HEX}"
-
1
REMOTE_NAME = /^(#{NAME})(:(#{TAG}))?(@#{DIGEST})?$/
-
-
end
-
-
1
Docker.freeze
-
-
# http://stackoverflow.com/questions/37861791/
-
# https://github.com/moby/moby/blob/master/image/spec/v1.1.md
-
# https://github.com/docker/distribution/blob/master/reference/reference.go
-
# frozen_string_literal: true
-
1
require 'rubygems/package' # Gem::Package::TarReader
-
1
require 'stringio'
-
-
1
module TarFile
-
-
1
class Reader
-
-
1
def initialize(tar_file)
-
4
io = StringIO.new(tar_file, 'r+t')
-
4
@reader = Gem::Package::TarReader.new(io)
-
end
-
-
1
def files
-
4
@reader.each.with_object({}) do |entry,memo|
-
5
filename = entry.full_name
-
5
content = entry.read || '' # avoid nil
-
5
memo[filename] = content
-
end
-
end
-
-
end
-
-
end
-
1
require 'rubygems/package' # Gem::Package::TarWriter
-
1
require 'stringio'
-
-
1
module TarFile
-
-
1
class Writer
-
-
1
def initialize
-
5
@tar_file = StringIO.new('')
-
5
@writer = Gem::Package::TarWriter.new(@tar_file)
-
end
-
-
1
def write(filename, content)
-
6
size = content.bytesize
-
6
@writer.add_file_simple(filename, 0o644, size) do |fd|
-
6
fd.write(content)
-
end
-
end
-
-
1
def tar_file
-
4
@tar_file.string
-
end
-
-
end
-
-
end
-
# frozen_string_literal: true
-
1
require_relative 'gnu_unzip'
-
1
require_relative 'gnu_zip'
-
1
require_relative 'tarfile_reader'
-
1
require_relative 'tarfile_writer'
-
-
1
module TGZ
-
-
1
def self.of(files)
-
2
writer = TarFile::Writer.new
-
2
files.each do |filename, content|
-
2
writer.write(filename, content)
-
end
-
2
Gnu.zip(writer.tar_file)
-
end
-
-
1
def self.files(tgz)
-
2
unzipped = Gnu.unzip(tgz)
-
2
reader = TarFile::Reader.new(unzipped)
-
2
reader.files.each.with_object({}) do |(filename,content),memo|
-
2
memo[filename] = content
-
end
-
end
-
-
end
-
# frozen_string_literal: true
-
1
require_relative 'empty_binding'
-
1
require_relative 'rag_lambdas'
-
1
require 'json'
-
-
1
class TrafficLight
-
-
1
class Fault < RuntimeError
-
1
def initialize(properties)
-
@properties = properties
-
end
-
1
attr_reader :properties
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
1
def initialize(context)
-
60
@context = context
-
60
@rag_lambdas = RagLambdas.new
-
end
-
-
1
def colour(image_name, stdout, stderr, status)
-
[ self[image_name].call(stdout, stderr, status), {} ]
-
rescue Fault => error
-
fault_info = {
-
call:"TrafficLight.colour(image_name,stdout,stderr,status)",
-
args:{
-
image_name:image_name,
-
stdout:stdout.lines,
-
stderr:stderr.lines,
-
status:status
-
},
-
exception:error.properties
-
}
-
logger.log(JSON.pretty_generate(fault_info))
-
[ 'faulty', fault_info ]
-
end
-
-
1
private
-
-
1
def [](image_name)
-
light = @rag_lambdas[image_name]
-
return light unless light.nil?
-
lambda_source = checked_read_lambda_source(image_name)
-
fn = checked_eval(lambda_source)
-
@rag_lambdas.compute(image_name) {
-
lambda { |stdout,stderr,status|
-
colour = checked_call(fn, lambda_source, stdout, stderr, status)
-
checked_colour(colour, lambda_source)
-
}
-
}
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
1
def checked_read_lambda_source(image_name)
-
command = [
-
'docker run --rm --entrypoint=cat',
-
image_name,
-
RAG_LAMBDA_FILENAME
-
].join(SPACE)
-
stdout,stderr,status = sheller.capture(command)
-
if status === 0
-
message = "Read red-amber-green lambda for #{image_name}"
-
logger.log(message)
-
stdout
-
else
-
fail Fault.new({
-
context: "image_name must have #{RAG_LAMBDA_FILENAME} file",
-
command: command,
-
stdout: stdout.lines,
-
stderr: stderr.lines,
-
status: status
-
})
-
end
-
end
-
-
1
RAG_LAMBDA_FILENAME = '/usr/local/bin/red_amber_green.rb'
-
-
1
SPACE = ' '
-
-
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
1
def checked_eval(lambda_source)
-
Empty.binding.eval(lambda_source)
-
rescue Exception => error
-
fail Fault.new({
-
context: "exception when eval'ing lambda source",
-
lambda_source: lambda_source.lines,
-
class: error.class.name,
-
message: error.message.lines
-
})
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
1
def checked_call(fn, lambda_source, stdout, stderr, status)
-
fn.call(stdout,stderr,status.to_i).to_s
-
rescue Exception => error
-
fail Fault.new({
-
context: "exception when calling lambda source",
-
lambda_source: lambda_source.lines,
-
class: error.class.name,
-
message: error.message.lines
-
})
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
1
def checked_colour(colour, lambda_source)
-
if LEGAL_COLOURS.include?(colour)
-
colour
-
else
-
fail Fault.new({
-
context: "illegal colour; must be one of ['red','amber','green']",
-
illegal_colour: colour,
-
lambda_source: lambda_source.lines
-
})
-
end
-
end
-
-
1
LEGAL_COLOURS = [ 'red', 'amber', 'green' ]
-
-
# - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
1
def logger
-
@context.logger
-
end
-
-
1
def sheller
-
@context.sheller
-
end
-
-
end
-
# frozen_string_literal: true
-
1
module Utf8
-
-
1
def self.clean(s)
-
# force an encoding change
-
# if encoding is already utf-8 then encoding
-
# to utf-8 is a no-op and invalid byte sequences
-
# are not detected.
-
1
s = s.encode('UTF-16', 'UTF-8', :invalid => :replace, :replace => '')
-
1
s = s.encode('UTF-8', 'UTF-16')
-
end
-
-
end
-
-
# http://robots.thoughtbot.com/fight-back-utf-8-invalid-byte-sequences
-
# frozen_string_literal: true
-
-
1
module DisplayNames
-
-
1
ALPINE = 'C#, NUnit'
-
1
DEBIAN = 'C (gcc), assert'
-
1
UBUNTU = 'C (clang), assert'
-
-
end
-
# frozen_string_literal: true
-
-
1
module Test
-
1
module Data
-
1
module ImageNames
-
-
TAG_NO_DIGEST_NO =
-
1
%w(
-
cdf/gcc_assert
-
quay.io/cdf/gcc_assert
-
quay.io:8080/cdf/gcc_assert
-
localhost/cdf/gcc_assert
-
localhost:80/cdf/gcc_assert
-
gcc_assert
-
localhost/cdf/gcc_assert
-
localhost:23/cdf/gcc_assert
-
quay.io/cdf/gcc_assert
-
quay.io:80/cdf/gcc_assert
-
)
-
-
TAG_YES_DIGEST_NO =
-
1
[ "gcc_assert:#{'x'*127}" ] +
-
%w(
-
cdf/gcc_assert:latest
-
quay.io/cdf/gcc_assert:latest
-
quay.io:8080/cdf/gcc_assert:12
-
localhost/cdf/gcc_assert:tag
-
localhost:80/cdf/gcc_assert:1.2.3
-
gcc_assert:_
-
gcc_assert:2
-
gcc_assert:a
-
gcc_assert:A
-
gcc_assert:1.2
-
gcc_assert:1-2
-
cdf/gcc__assert:x
-
cdf/gcc__sd.a--ssert:latest
-
localhost/cdf/gcc_assert:latest
-
localhost:23/cdf/gcc_assert:latest
-
quay.io/cdf/gcc_assert:latest
-
quay.io:80/cdf/gcc_assert:latest
-
localhost/cdf/gcc__assert:x
-
localhost:23/cdf/gcc__assert:x
-
quay.io/cdf/gcc__assert:x
-
quay.io:80/cdf/gcc__assert:x
-
localhost/cdf/gcc__sd.a--ssert:latest
-
localhost:23/cdf/gcc__sd.a--ssert:latest
-
quay.io/cdf/gcc__sd.a--ssert:latest
-
quay.io:80/cdf/gcc__sd.a--ssert:latest
-
a-b-c:80/cdf/gcc__sd.a--ssert:latest
-
a.b.c:80/cdf/gcc__sd.a--ssert:latest
-
A.B.C:80/cdf/gcc__sd.a--ssert:latest
-
)
-
-
TAG_YES_DIGEST_YES =
-
1
%w(
-
localhost:80/gcc_assert:tag@sha2-s1+s2.s3_s5:12345678901234567890123456789012
-
localhost:80/cdf/gcc_assert:tag@sha2-s1+s2.s3_s5:12345678901234567890123456789012
-
quay.io:80/gcc_assert:latest@sha2-s1+s2.s3_s5:12345678901234567890123456789012
-
quay.io:80/gcc_assert:latest@sha2-s1+s2.s3_s5:123456789012345678901234567890123456789
-
quay.io:80/cdf/gcc_assert:latest@sha2-s1+s2.s3_s5:123456789012345678901234567890123456789
-
q.uay.io:80/cdf/gcc_assert:latest@sha2-s1+s2.s3_s5:123456789012345678901234567890123456789
-
)
-
-
TAG_NO_DIGEST_YES =
-
1
%w(
-
gcc_assert@sha256:12345678901234567890123456789012
-
gcc_assert@sha2-s1+s2.s3_s5:12345678901234567890123456789012
-
localhost/gcc_assert@sha2-s1+s2.s3_s5:12345678901234567890123456789012
-
localhost:80/gcc_assert@sha2-s1+s2.s3_s5:12345678901234567890123456789012
-
quay.io/gcc_assert@sha2-s1+s2.s3_s5:12345678901234567890123456789012
-
quay.io:80/gcc_assert@sha2-s1+s2.s3_s5:12345678901234567890123456789012
-
)
-
-
# - - - - - - - - - - - - - - - - - - - - - -
-
-
1
HEX = 'D'
-
-
MALFORMED =
-
[
-
1
nil,
-
'<none>', # [docker images] gives this
-
'', # nothing!
-
'_', # host-name cannot start with separator
-
'name_', # host-name cannot end with separator
-
';;;', # host-name illegal char
-
'ALPHA/name', # no uppercase in host-name
-
'gcc/Assert', # no uppercase in remote-name
-
'alpha/name_', # remote-name cannot end in separator
-
'alpha/_name', # remote-name cannot begin with separator
-
'gcc:.', # tag can't start with .
-
'gcc:-', # tag can't start with -
-
'gcc:{}', # {} is illegal tag
-
"gcc:#{'x'*129}", # tag too long
-
'-/gcc/assert:23', # - is illegal host-name
-
'-x/gcc/assert:23', # -x is illegal host-name
-
'x-/gcc/assert:23', # x- is illegal host-name
-
'/gcc/assert', # remote-name can't start with /
-
"gcc@sha256:#{HEX*31}", # digest-hex is too short
-
"gcc!sha256-2:#{HEX*32}", # digest must start with @
-
"gcc@256:#{HEX*32}", # digest-component must start with letter
-
"gcc@sha256-2:#{HEX*32}", # digest-component must start with letter
-
"gcc@sha256#{HEX*32}", # hex-digits must start with :
-
]
-
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
require_relative 'test_base'
-
-
1
class DoubleLanguagesStartPointsTest < TestBase
-
-
1
def self.id58_prefix
-
12
'DDx'
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - -
-
-
1
test 'as3', %w(
-
its alive ) do
-
1
assert languages_start_points.alive?.is_a?(TrueClass)
-
end
-
-
1
test 'as4', %w(
-
its ready ) do
-
1
assert languages_start_points.ready?.is_a?(TrueClass)
-
end
-
-
1
test 'as5', %w(
-
sha is SHA of git commit which created docker image
-
) do
-
1
assert_sha(languages_start_points.sha)
-
end
-
-
end
-
# frozen_string_literal: true
-
1
require 'json'
-
-
1
class BashShellerStub
-
-
1
def initialize
-
10
@stubs = []
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - - -
-
-
1
def teardown
-
4
unless uncaught_exception?
-
3
unless @stubs === []
-
1
pretty = JSON.pretty_generate(@stubs)
-
1
raise "#{ENV['ID58']}: uncalled stubs(#{pretty})"
-
end
-
end
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - - -
-
-
1
def capture(command)
-
15
if block_given?
-
8
stub = yield
-
8
@stubs << {
-
command:command,
-
stdout:stub[0],
-
stderr:stub[1],
-
status:stub[2]
-
}
-
else
-
7
matching_stub(command)
-
end
-
end
-
-
1
private
-
-
1
def matching_stub(command)
-
7
stub = @stubs.shift
-
7
if stub.nil?
-
1
raise [
-
self.class.name,
-
"capture(command) - no stub",
-
"actual-command: #{command}",
-
].join("\n") + "\n"
-
end
-
6
unless command === stub[:command]
-
1
raise [
-
self.class.name,
-
"capture(command) - does not match stub",
-
" actual-command:#{command}:",
-
"stubbed-command:#{stub[:command]}:"
-
].join("\n") + "\n"
-
end
-
5
[stub[:stdout], stub[:stderr], stub[:status]]
-
end
-
-
1
def uncaught_exception?
-
4
$!
-
end
-
-
end
-
# frozen_string_literal: true
-
-
1
class ProcessAdapterStub
-
-
1
def initialize
-
1
@stubs = {}
-
end
-
-
1
def method_missing(name, *_args)
-
2
if block_given?
-
1
@stubs[name] = yield
-
else
-
1
@stubs[name]
-
end
-
end
-
-
end
-
# frozen_string_literal: true
-
1
require 'ostruct'
-
-
1
class RackRequestStub
-
-
1
def initialize(env)
-
@env = env
-
end
-
-
1
def body
-
OpenStruct.new(read:@env[:body])
-
end
-
-
1
def path_info
-
"/#{@env[:path_info]}"
-
end
-
-
end
-
# frozen_string_literal: true
-
-
1
class StdoutLoggerSpy
-
-
1
def initialize
-
63
@logged = ''
-
end
-
-
1
attr_reader :logged
-
-
1
def log(message)
-
4
unless message.empty?
-
3
message += "\n" if message[-1] != "\n"
-
3
@logged += message
-
end
-
end
-
-
end
-
# frozen_string_literal: true
-
-
1
class SynchronousThreader
-
-
1
attr_reader :called
-
-
1
def initialize
-
@called = false
-
end
-
-
1
def thread(&block)
-
@called = true
-
block.call
-
end
-
-
end
-
# frozen_string_literal: true
-
-
1
class TrafficLightStub
-
-
1
def initialize(colour = 'red')
-
3
@stubbed = colour
-
end
-
-
1
def colour(_image_name, _stdout, _stderr, _status)
-
@stubbed
-
end
-
-
1
@@red = TrafficLightStub.new('red')
-
1
@@amber = TrafficLightStub.new('amber')
-
1
@@green = TrafficLightStub.new('green')
-
-
1
def self.red ; @@red ; end
-
1
def self.amber; @@amber; end
-
1
def self.green; @@green; end
-
-
end
-
1
require 'minitest/autorun'
-
1
require_relative 'require_source'
-
-
1
class Id58TestBase < MiniTest::Test
-
-
1
def initialize(arg)
-
60
@_id58 = nil
-
60
@_name58 = nil
-
60
super
-
end
-
-
1
@@args = (ARGV.sort.uniq - ['--']) # eg 2m4
-
1
@@seen_ids = {}
-
1
@@timings = {}
-
-
# - - - - - - - - - - - - - - - - - - - - - -
-
-
1
def self.define_test(os, display_name, id58_suffix, *lines, &test_block)
-
60
src = test_block.source_location
-
60
src_file = File.basename(src[0])
-
60
src_line = src[1].to_s
-
60
id58 = checked_id58(os, id58_suffix, lines)
-
60
if @@args === [] || @@args.any?{ |arg| id58.include?(arg) }
-
60
name58 = lines.join(space = ' ')
-
60
execute_around = lambda {
-
60
ENV['ID58'] = id58
-
60
if ENV['SHOW_TEST_IDS'] === 'true'
-
p [id58,src_file].join(':')
-
end
-
60
@_os = os
-
60
@_display_name = display_name
-
60
@_id58 = id58
-
60
@_name58 = name58
-
60
id58_setup
-
begin
-
60
t1 = Time.now
-
60
self.instance_eval(&test_block)
-
60
t2 = Time.now
-
60
info = [ id58, os.to_s, display_name, name58, "#{src_file}:#{src_line}" ].join(' - ')
-
60
@@timings[info] = (t2 - t1)
-
ensure
-
60
puts $!.message unless $!.nil?
-
60
id58_teardown
-
end
-
}
-
60
name = "id='#{id58}'\nos=#{os}\n'#{name58}'"
-
60
define_method("test_\n#{name}".to_sym, &execute_around)
-
end
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - -
-
-
skipped
# :nocov:
-
skipped
ObjectSpace.define_finalizer(self, proc {
-
skipped
slow = @@timings.select{ |_name,secs| secs > 0.000 }
-
skipped
sorted = slow.sort_by{ |name,secs| -secs }.to_h
-
skipped
size = sorted.size < 25 ? sorted.size : 25
-
skipped
puts
-
skipped
puts "Slowest #{size} tests are..." if size != 0
-
skipped
sorted.each_with_index { |(name,secs),index|
-
skipped
puts "%3.4f - %-72s" % [secs,name]
-
skipped
break if index === size
-
skipped
}
-
skipped
puts
-
skipped
})
-
skipped
# :nocov:
-
-
# - - - - - - - - - - - - - - - - - - - - - -
-
-
ID58_ALPHABET = %w{
-
1
0 1 2 3 4 5 6 7 8 9
-
A B C D E F G H J K L M N P Q R S T U V W X Y Z
-
a b c d e f g h j k l m n p q r s t u v w x y z
-
}.join.freeze
-
-
1
def self.id58?(s)
-
120
s.is_a?(String) &&
-
360
s.chars.all?{ |ch| ID58_ALPHABET.include?(ch) }
-
end
-
-
1
def self.checked_id58(os, id58_suffix, lines)
-
60
method = 'def self.id58_prefix'
-
60
pointer = ' ' * method.index('.') + '!'
-
60
pointee = (['',pointer,method,'','']).join("\n")
-
60
pointer.prepend("\n\n")
-
60
raise "#{pointer}missing#{pointee}" unless respond_to?(:id58_prefix)
-
60
raise "#{pointer}empty#{pointee}" if id58_prefix === ''
-
60
raise "#{pointer}not id58#{pointee}" unless id58?(id58_prefix)
-
-
60
method = "test '#{id58_suffix}',"
-
60
pointer = ' ' * method.index("'") + '!'
-
60
proposition = lines.join(space = ' ')
-
60
pointee = ['',pointer,method,"'#{proposition}'",'',''].join("\n")
-
60
id58 = id58_prefix + id58_suffix
-
60
pointer.prepend("\n\n")
-
60
raise "#{pointer}empty#{pointee}" if id58_suffix === ''
-
60
raise "#{pointer}not id58#{pointee}" unless id58?(id58_suffix)
-
60
raise "#{pointer}duplicate#{pointee}" if seen?(id58, os)
-
60
raise "#{pointer}overlap#{pointee}" if id58_prefix[-2..-1] === id58_suffix[0..1]
-
60
seen(id58, os)
-
60
id58
-
end
-
-
1
def self.seen?(id58, os)
-
60
seen = @@seen_ids[id58] || []
-
60
seen.include?(os)
-
end
-
-
1
def self.seen(id58, os)
-
60
@@seen_ids[id58] ||= []
-
60
@@seen_ids[id58] << os
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - -
-
-
1
def id58_setup
-
end
-
-
1
def id58_teardown
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - -
-
-
1
def os
-
@_os
-
end
-
-
1
def display_name
-
@_display_name
-
end
-
-
1
def id58
-
2
@_id58
-
end
-
-
skipped
# :nocov:
-
skipped
def name58
-
skipped
@_name58
-
skipped
end
-
skipped
# :nocov:
-
-
end
-
# frozen_string_literal: true
-
-
1
def require_source(required)
-
14
require_relative "../app/code/#{required}"
-
end
-
# frozen_string_literal: true
-
1
require_relative 'test_base'
-
-
1
class ServerBashShellerStubTest < TestBase
-
-
1
def self.id58_prefix
-
24
'F03'
-
end
-
-
1
def id58_setup
-
6
@sheller = BashShellerStub.new
-
end
-
-
1
attr_reader :sheller
-
-
# - - - - - - - - - - - - - - -
-
-
1
test '4A5',
-
%w(
-
teardown does not raise
-
when no capture()s are stubbed
-
and no capture()s are made
-
) do
-
1
sheller.teardown
-
end
-
-
# - - - - - - - - - - - - - - -
-
-
1
test '652', %w(
-
capture() raises when capture() is not stubbed
-
) do
-
2
assert_raises { sheller.capture(pwd) }
-
end
-
-
# - - - - - - - - - - - - - - -
-
-
1
test '181', %w(
-
capture() raises when capture() is stubbed but for a different command
-
) do
-
2
sheller.capture(pwd) { [wd, stderr='', success] }
-
2
assert_raises { sheller.capture(not_pwd = "cd #{wd}") }
-
end
-
-
# - - - - - - - - - - - - - - -
-
-
1
test 'B4E',
-
%w(
-
teardown does not raise
-
when one capture() is stubbed
-
and a matching capture() is made
-
) do
-
2
sheller.capture(pwd) { [wd, stderr='', success] }
-
1
stdout,stderr,status = sheller.capture('pwd')
-
1
assert_equal wd, stdout
-
1
assert_equal '', stderr
-
1
assert_equal success, status
-
1
sheller.teardown
-
end
-
-
# - - - - - - - - - - - - - - -
-
-
1
test 'D0C',
-
%w(
-
teardown raises
-
when a capture() is stubbed
-
and no capture() is made
-
) do
-
2
sheller.capture(pwd) { [wd, stderr='', success] }
-
2
assert_raises { sheller.teardown }
-
end
-
-
# - - - - - - - - - - - - - - -
-
-
1
test '470', %w(
-
teardown does not raise
-
when there is an uncaught exception
-
) do
-
2
sheller.capture(pwd) { [wd, stderr='', success] }
-
1
error = assert_raises {
-
begin
-
1
raise 'forced'
-
ensure
-
1
sheller.teardown
-
end
-
}
-
1
assert_equal 'forced', error.message
-
end
-
-
1
private
-
-
1
def pwd
-
5
'pwd'
-
end
-
-
1
def wd
-
6
'/Users/jonjagger/repos/web'
-
end
-
-
1
def success
-
5
0
-
end
-
-
end
-
# frozen_string_literal: true
-
1
require_relative 'test_base'
-
-
1
class ServerBashShellerTest < TestBase
-
-
1
def self.id58_prefix
-
12
'C89'
-
end
-
-
# - - - - - - - - - - - - - - - - -
-
-
1
test '243',
-
%w( when capture(command) raises an exception,
-
then the exception is untouched
-
then nothing is logged
-
) do
-
2
error = assert_raises(Errno::ENOENT) { sheller.capture('xxx Hello') }
-
1
expected = 'No such file or directory - xxx'
-
1
assert_equal expected, error.message, :error_message
-
1
assert log.empty?, log
-
end
-
-
# - - - - - - - - - - - - - - - - -
-
-
1
test '244',
-
%w(
-
when capture(command)'s status is zero,
-
it logs nothing,
-
it returns [stdout,stderr,status],
-
) do
-
1
stdout,stderr,status = sheller.capture('printf Specs')
-
1
assert_equal 'Specs', stdout, :stdout
-
1
assert_equal '', stderr, :stderr
-
1
assert_equal 0, status, :status
-
1
assert log.empty?, log
-
end
-
-
# - - - - - - - - - - - - - - - - -
-
-
1
test '245',
-
%w(
-
when capture(command)'s status is non-zero,
-
it does not raise,
-
it logs [command,stdout,stderr,status],
-
it returns [stdout,stderr,status],
-
) do
-
1
command = 'printf Croc && >&2 printf Fish && false'
-
1
stdout,stderr,status = sheller.capture(command)
-
1
assert_equal 'Croc', stdout, :stdout
-
1
assert_equal 'Fish', stderr, :stderr
-
1
assert_equal 1, status, :status
-
1
assert logged?("command:#{command}:"), log
-
1
assert logged?('stdout:Croc:'), log
-
1
assert logged?('stderr:Fish:'), log
-
1
assert logged?('status:1:'), log
-
end
-
-
1
private
-
-
1
def sheller
-
3
context.sheller
-
end
-
-
end
-
# frozen_string_literal: true
-
1
require_relative 'test_base'
-
1
require_source 'files_delta'
-
-
1
class ServerFilesDeltaTest < TestBase
-
-
1
def self.id58_prefix
-
20
'5C2'
-
end
-
-
1
include FilesDelta
-
-
# - - - - - - - - - - - - - - - - -
-
-
1
test 'E76', %w( unchanged content ) do
-
1
was_files = { 'wibble.txt' => 'hello' }
-
1
now_files = { 'wibble.txt' => intact('hello') }
-
1
created,deleted,changed = files_delta(was_files, now_files)
-
1
assert_equal({}, created)
-
1
assert_equal({}, deleted)
-
1
assert_equal({}, changed)
-
end
-
-
# - - - - - - - - - - - - - - - - -
-
-
1
test 'E77', %w( changed content ) do
-
1
was_files = { 'wibble.txt' => 'hello' }
-
1
now_files = { 'wibble.txt' => intact('hello, world') }
-
1
created,deleted,changed = files_delta(was_files, now_files)
-
1
assert_equal({}, created)
-
1
assert_equal({}, deleted)
-
1
assert_equal({'wibble.txt' => intact('hello, world')}, changed)
-
end
-
-
# - - - - - - - - - - - - - - - - -
-
-
1
test 'E78', %w( deleted content ) do
-
1
filename = 'wibble.txt'
-
1
content = 'hello'
-
1
was_files = { filename => content }
-
1
now_files = {}
-
1
created,deleted,changed = files_delta(was_files, now_files)
-
expected_deleted = {
-
1
filename => {
-
'content' => content
-
}
-
}
-
1
assert_equal({}, created)
-
1
assert_equal(expected_deleted, deleted)
-
1
assert_equal({}, changed)
-
end
-
-
# - - - - - - - - - - - - - - - - -
-
-
1
test 'E79', %w( new content ) do
-
1
was_files = {}
-
1
now_files = { 'wibble.txt' => intact('hello') }
-
1
created,deleted,changed = files_delta(was_files, now_files)
-
1
assert_equal({'wibble.txt' => intact('hello')}, created)
-
1
assert_equal({}, deleted)
-
1
assert_equal({}, changed)
-
end
-
-
# - - - - - - - - - - - - - - - - -
-
-
1
test 'E80', %w( new empty content ) do
-
1
was_files = {}
-
1
now_files = { 'empty.file' => intact('') }
-
1
created,deleted,changed = files_delta(was_files, now_files)
-
1
assert_equal({'empty.file' => intact('')}, created)
-
1
assert_equal({}, deleted)
-
1
assert_equal({}, changed)
-
end
-
-
end
-
# frozen_string_literal: true
-
1
require_relative 'test_base'
-
1
require_source 'gnu_zip'
-
1
require_source 'gnu_unzip'
-
-
1
class ServerGnuZipTest < TestBase
-
-
1
def self.id58_prefix
-
8
'Cw4'
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - -
-
-
1
test '4A1', %w( simple gzip round-trip of non-empty string ) do
-
1
expected = 'sdgfadsfghfghsfhdfghdfghdfgh'
-
1
zipped = Gnu.zip(expected)
-
1
actual = Gnu.unzip(zipped)
-
1
assert_equal expected, actual
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - -
-
-
1
test '4A2', %w( simple gzip round-trip of empty string ) do
-
1
expected = ''
-
1
zipped = Gnu.zip(expected)
-
1
actual = Gnu.unzip(zipped)
-
1
assert_equal expected, actual
-
end
-
-
end
-
# frozen_string_literal: true
-
1
require_relative 'test_base'
-
-
1
class ServerId58TestTest < TestBase
-
-
1
def self.id58_prefix
-
24
'89c'
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - -
-
-
1
test 'C80',
-
'test-id is available via environment variable' do
-
1
assert_equal '89cC80', ENV['ID58']
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - -
-
-
1
test '57B',
-
'test-id is also available via a method',
-
'and is the id58_prefix concatenated with the test-id' do
-
1
assert_equal '89c57B', id58
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - -
-
-
1
test '18F',
-
'test-name is available via a method' do
-
1
assert_equal 'test-name is available via a method', name58
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - -
-
-
1
test 'D30',
-
'test-name can be long',
-
'and split over many',
-
'comma separated lines',
-
'and will automatically be',
-
'joined with spaces' do
-
1
expected = [
-
'test-name can be long',
-
'and split over many',
-
'comma separated lines',
-
'and will automatically be',
-
'joined with spaces'
-
].join(' ')
-
1
assert_equal expected, name58
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - -
-
-
1
test 'D31', %w(
-
test-name can be long
-
and split over many lines
-
with %w syntax
-
and will automatically be
-
joined with spaces
-
) do
-
1
expected = [
-
'test-name can be long',
-
'and split over many lines',
-
'with %w syntax',
-
'and will automatically be',
-
'joined with spaces'
-
].join(' ')
-
1
assert_equal expected, name58
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - -
-
-
1
test 'e3a', %w( digits can be UPPERCASE or lowercase ) do
-
1
assert_equal '89ce3a', ENV['ID58']
-
1
assert_equal '89ce3a', id58
-
end
-
-
end
-
# frozen_string_literal: true
-
1
require_relative 'test_base'
-
-
1
class ServerNodeTest < TestBase
-
-
1
def self.id58_prefix
-
16
'3q1'
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - -
-
-
1
test 'Ps3', %w( image_names are retrieved from the node via docker image ls call ) do
-
1
stub_sheller_capture
-
2
sheller.capture(DOCKER_IMAGE_LS_COMMAND) { [expected.join("\n"),'',0] }
-
1
actual = node.image_names
-
1
assert_equal expected, actual
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - -
-
-
1
test 'Ps4', %w( <none>:<none> image_names are filtered out ) do
-
1
stub_sheller_capture
-
1
tainted = (expected + ['<none>:<none>']*3).shuffle
-
2
sheller.capture(DOCKER_IMAGE_LS_COMMAND) { [tainted.join("\n"),'',0] }
-
1
actual = node.image_names
-
1
assert_equal expected, actual
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - -
-
-
1
test 'Ps5', %w( image_names populate puller in config.ru ) do
-
1
stub_sheller_capture
-
2
sheller.capture(DOCKER_IMAGE_LS_COMMAND) { [expected.join("\n"),'',0] }
-
1
node.image_names.each do |image_name|
-
6
puller.add(image_name)
-
end
-
1
assert_equal expected, puller.image_names
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - -
-
-
1
test 'Ps6', %w( when docker image ls call fails exception is raised ) do
-
1
stub_sheller_capture
-
2
sheller.capture(DOCKER_IMAGE_LS_COMMAND) { ['','stderr-info',1] }
-
2
error = assert_raises { node.image_names }
-
1
assert_equal 'stderr-info', error.message
-
end
-
-
1
private
-
-
1
def stub_sheller_capture
-
8
context.instance_exec { @sheller = BashShellerStub.new }
-
end
-
-
1
DOCKER_IMAGE_LS_COMMAND = "docker image ls --format '{{.Repository}}:{{.Tag}}'"
-
-
1
def expected
-
6
%w(
-
cyberdojo/avatars:1fce37b
-
cyberdojo/saver:723349e
-
openjdk:13-jdk-alpine
-
cyberdojo/versioner:latest
-
cyberdojo/commander:b291513
-
cyberdojo/web-base:63adedc
-
).sort
-
end
-
-
1
def node
-
4
context.node
-
end
-
-
1
def puller
-
7
context.puller
-
end
-
-
1
def sheller
-
4
context.sheller
-
end
-
-
end
-
# frozen_string_literal: true
-
1
require_relative 'test_base'
-
-
1
class ServerProberTest < TestBase
-
-
1
def self.id58_prefix
-
12
'6de'
-
end
-
-
# - - - - - - - - - - - - - - - - -
-
-
1
test '190', %w(
-
alive? is true
-
) do
-
1
assert prober.alive?.is_a?(TrueClass)
-
end
-
-
# - - - - - - - - - - - - - - - - -
-
-
1
test '191', %w(
-
ready? is true
-
) do
-
1
assert prober.ready?.is_a?(TrueClass)
-
end
-
-
# - - - - - - - - - - - - - - - - -
-
-
1
test '192', %w(
-
sha is SHA of git commit which created docker image
-
) do
-
1
assert_sha(prober.sha)
-
end
-
-
end
-
# frozen_string_literal: true
-
1
require_relative 'test_base'
-
-
1
class ServerProcessAdapterStubTest < TestBase
-
-
1
def self.id58_prefix
-
4
'A3r'
-
end
-
-
# - - - - - - - - - - - - - - -
-
-
1
test 'Kb1',
-
%w( use with a block to supply the stub, use without a block gets the stub ) do
-
1
stub = ProcessAdapterStub.new
-
2
stub.spawn { 42 }
-
1
assert_equal 42, stub.spawn
-
end
-
-
end
-
# frozen_string_literal: true
-
1
require_relative 'test_base'
-
1
require_source 'random_hex'
-
1
require 'benchmark'
-
-
1
class ServerRandomHexTest < TestBase
-
-
1
def self.id58_prefix
-
8
'4a7'
-
end
-
-
# - - - - - - - - - - - - - - - - -
-
-
1
test 'c91', %w( hex_id(8) is size 8, each char is hex-digit ) do
-
1
512.times do
-
512
size = 8
-
512
assert is_hex?(size, RandomHex.id(size), 0)
-
512
assert is_hex?(size, v1(size), 1)
-
512
assert is_hex?(size, v2(size), 2)
-
512
assert is_hex?(size, v3(size), 3)
-
end
-
end
-
-
# - - - - - - - - - - - - - - - - -
-
-
1
test 'c92', %w( size=0 doesn't work but I don't need that anyway ) do
-
2
error = assert_raises(ArgumentError) { RandomHex.id(0) }
-
1
assert_equal 'wrong number of arguments (given 1, expected 0)', error.message
-
end
-
-
# - - - - - - - - - - - - - - - - -
-
-
=begin
-
test 'c94', %w(
-
its the fastest of the 4 algorithms I tried
-
but environment too variable for consistent results
-
) do
-
n,size = 512,8
-
t3 = Benchmark.realtime { n.times { v3(size) } }
-
t2 = Benchmark.realtime { n.times { v2(size) } }
-
t1 = Benchmark.realtime { n.times { v1(size) } }
-
# occasionally t1 is close. Do t0 last to give it best chance!
-
t0 = Benchmark.realtime { n.times { RandomHex.id(size) } }
-
assert t0 < t1, "t=#{t0} , t1 is faster #{t1} "
-
assert t0 < t2, "t=#{t0} , t2 is faster #{t2} "
-
assert t0 < t3, "t=#{t0} , t3 is faster #{t3} "
-
end
-
=end
-
-
# - - - - - - - - - - - - - - - - -
-
-
1
private
-
-
1
def is_hex?(n, s, version)
-
2048
assert s.is_a?(String), "v(#{version}) not a String"
-
2048
assert_equal n, s.size, "v(#{version}) wrong size"
-
2048
s.each_char do |ch|
-
16384
assert HEX_DIGITS.include?(ch), "v(#{version}) not a hex-digit #{ch}"
-
end
-
end
-
-
1
HEX_DIGITS = [*('a'..'z'),*('A'..'Z'),*('0'..'9')]
-
-
1
def v1(n)
-
512
HEX_DIGITS.shuffle[0,n].join
-
end
-
-
1
def v2(n)
-
4608
n.times.map{HEX_DIGITS.sample}.join
-
end
-
-
1
def v3(n)
-
4608
Array.new(n){HEX_DIGITS.sample}.join
-
end
-
-
end
-
# frozen_string_literal: true
-
1
require_relative 'test_base'
-
1
require_source 'sandbox'
-
-
1
class ServerSandboxTest < TestBase
-
-
1
def self.id58_prefix
-
12
'd2b'
-
end
-
-
# - - - - - - - - - - - - - - - - - -
-
-
1
test 'd55', %w( empty files no-ops ) do
-
1
assert_equal({}, Sandbox.in({}))
-
1
assert_equal({}, Sandbox.out({}))
-
end
-
-
# - - - - - - - - - - - - - - - - - -
-
-
1
test 'd56', %w(
-
Sandbox.in prefixes filenames with sandbox/
-
note there is no leading slash
-
because tar prefers relative paths
-
) do
-
1
greetings = 'greetings earthlings...'
-
1
code = '#include <stdio.h>'
-
1
sandboxed = Sandbox.in({
-
'hello.txt' => greetings,
-
'hiker.c' => code
-
})
-
expected = {
-
1
'sandbox/hello.txt' => greetings,
-
'sandbox/hiker.c' => code
-
}
-
1
assert_equal expected, sandboxed
-
end
-
-
# - - - - - - - - - - - - - - - - - -
-
-
1
test 'd57', %w( Sandbox.out reverses Sandbox.in ) do
-
1
vogon_greeting = 'People of earth, your attention please'
-
1
header = '#include <stdlib.h>'
-
1
unsandboxed = Sandbox.out({
-
'sandbox/hello.txt' => vogon_greeting,
-
'sandbox/hiker.h' => header
-
})
-
expected = {
-
1
'hello.txt' => vogon_greeting,
-
'hiker.h' => header
-
}
-
1
assert_equal expected, unsandboxed
-
end
-
-
end
-
1
require_relative 'test_base'
-
-
1
class ServerStdoutLoggerSpyTest < TestBase
-
-
1
def self.id58_prefix
-
12
'60e'
-
end
-
-
# - - - - - - - - - - - - - - - - -
-
-
1
test 'dF7', %w( log(s) is a no-op when s is empty ) do
-
1
assert_equal '', log('')
-
end
-
-
# - - - - - - - - - - - - - - - - -
-
-
1
test 'dF8', %w( log(s) logs s and a trailing newline when s does not end in a newline ) do
-
1
assert_equal "hello\n", log('hello')
-
end
-
-
# - - - - - - - - - - - - - - - - -
-
-
1
test 'dF9', %w( log(s) logs s as it is when s ends in a newline ) do
-
1
assert_equal "world\n", log("world\n")
-
end
-
-
1
private
-
-
1
def log(message)
-
3
spy = StdoutLoggerSpy.new
-
3
spy.log(message)
-
3
spy.logged
-
end
-
-
end
-
1
require_relative 'test_base'
-
1
require_source 'externals/stdout_logger'
-
-
1
class ServerStdoutLoggerTest < TestBase
-
-
1
def self.id58_prefix
-
12
'55t'
-
end
-
-
# - - - - - - - - - - - - - - - - -
-
-
1
test 'dF7', %w( log(s) is a no-op when s is empty ) do
-
1
assert_equal '', log('')
-
end
-
-
# - - - - - - - - - - - - - - - - -
-
-
1
test 'dF8', %w( log(s) logs s and a trailing newline when s does not end in a newline ) do
-
1
assert_equal "hello\n", log('hello')
-
end
-
-
# - - - - - - - - - - - - - - - - -
-
-
1
test 'dF9', %w( log(s) logs s as it is when s ends in a newline ) do
-
1
assert_equal "world\n", log("world\n")
-
end
-
-
1
private
-
-
1
def log(message)
-
3
captured_stdout do
-
3
logger = StdoutLogger.new
-
3
logger.log(message)
-
end
-
end
-
-
1
def captured_stdout
-
begin
-
3
old_stdout = $stdout
-
3
$stdout = StringIO.new('', 'w')
-
3
yield
-
3
captured = $stdout.string
-
ensure
-
3
$stdout = old_stdout
-
end
-
3
captured
-
end
-
-
end
-
# frozen_string_literal: true
-
1
require_relative 'test_base'
-
1
require_source 'synchronized_set'
-
-
1
class ServerSynchronizedSetTest < TestBase
-
-
1
def self.id58_prefix
-
20
'wK9'
-
end
-
-
# - - - - - - - - - - - - - - - - -
-
-
1
test 'd9f', %w(
-
initially empty
-
) do
-
1
s = SynchronizedSet.new
-
1
assert_equal [], s.to_a
-
end
-
-
# - - - - - - - - - - - - - - - - -
-
-
1
test 'd9g', %w(
-
values not added are not included
-
) do
-
1
s = SynchronizedSet.new
-
1
refute s.include?(42)
-
end
-
-
# - - - - - - - - - - - - - - - - -
-
-
1
test 'd9h', %w(
-
added values are included
-
) do
-
1
s = SynchronizedSet.new
-
1
s.add(42)
-
1
assert s.include?(42)
-
1
refute s.include?(24)
-
1
assert_equal [42], s.to_a
-
end
-
-
# - - - - - - - - - - - - - - - - -
-
-
1
test 'd9j', %w(
-
deleted values are not included
-
) do
-
1
s = SynchronizedSet.new
-
1
s.add(42)
-
1
s.add(24)
-
1
assert_equal [24,42], s.to_a
-
1
s.delete(42)
-
1
assert_equal [24], s.to_a
-
1
s.delete(24)
-
1
assert_equal [], s.to_a
-
end
-
-
# - - - - - - - - - - - - - - - - -
-
-
1
test 'd9k', %w(
-
add? returns non-nil when value is not already included,
-
returns nil when value is already included
-
) do
-
1
s = SynchronizedSet.new
-
1
refute_nil s.add?(42)
-
1
assert_equal [42], s.to_a
-
1
assert_nil s.add?(42)
-
1
assert_equal [42], s.to_a
-
end
-
-
end
-
# frozen_string_literal: true
-
1
require_relative 'test_base'
-
1
require_relative 'data/image_names'
-
1
require_source 'tagged_image_name'
-
-
1
class ServerTaggedImageNameTest < TestBase
-
-
1
def self.id58_prefix
-
20
'9g8'
-
end
-
-
# - - - - - - - - - - - - - - - - -
-
-
1
test '000', 'malformed_image_name' do
-
1
Test::Data::ImageNames::MALFORMED.each do |image_name|
-
23
refute Docker::image_name?(image_name), image_name
-
end
-
end
-
-
# - - - - - - - - - - - - - - - - -
-
-
1
test '001', %w( unchanged when a tag and no digest ) do
-
1
Test::Data::ImageNames::TAG_YES_DIGEST_NO.each do |image_name|
-
29
assert Docker::image_name?(image_name), image_name
-
29
expected = image_name
-
29
actual = Docker::tagged_image_name(image_name)
-
29
assert_equal expected, actual
-
end
-
end
-
-
# - - - - - - - - - - - - - - - - -
-
-
1
test '002', %w( unchanged when a tag and a digest ) do
-
1
Test::Data::ImageNames::TAG_YES_DIGEST_YES.each do |image_name|
-
6
assert Docker::image_name?(image_name), image_name
-
6
expected = image_name
-
6
actual = Docker::tagged_image_name(image_name)
-
6
assert_equal expected, actual
-
end
-
end
-
-
# - - - - - - - - - - - - - - - - -
-
-
1
test '003', %w( tagged with :latest when no tag and no digest ) do
-
1
Test::Data::ImageNames::TAG_NO_DIGEST_NO.each do |image_name|
-
10
assert Docker::image_name?(image_name), image_name
-
10
expected = image_name+':latest'
-
10
actual = Docker::tagged_image_name(image_name)
-
10
assert_equal expected, actual
-
end
-
end
-
-
# - - - - - - - - - - - - - - - - -
-
-
1
test '004', %w( tagged with :latest when no tag and a digest ) do
-
1
Test::Data::ImageNames::TAG_NO_DIGEST_YES.each do |image_name|
-
6
assert Docker::image_name?(image_name), image_name
-
6
at = image_name.index('@')
-
6
lhs,rhs = image_name[0..at-1], image_name[at..-1]
-
6
expected = "#{lhs}:latest#{rhs}"
-
6
actual = Docker::tagged_image_name(image_name)
-
6
assert_equal expected, actual
-
end
-
end
-
-
end
-
# frozen_string_literal: true
-
1
require_relative 'test_base'
-
1
require_source 'tarfile_reader'
-
1
require_source 'tarfile_writer'
-
-
1
class ServerTarFileTest < TestBase
-
-
1
def self.id58_prefix
-
12
'80B'
-
end
-
-
# - - - - - - - - - - - - - - - - - -
-
-
1
test '364', 'simple tar round-trip' do
-
1
writer = TarFile::Writer.new
-
1
expected = {
-
'hello.txt' => 'greetings earthlings...',
-
'hiker.c' => '#include <stdio.h>'
-
}
-
1
expected.each do |filename, content|
-
2
writer.write(filename, content)
-
end
-
1
reader = TarFile::Reader.new(writer.tar_file)
-
1
actual = reader.files
-
1
assert_equal expected, actual
-
end
-
-
# - - - - - - - - - - - - - - - - - -
-
-
1
test '365', 'writing content where .size != .bytesize does not throw' do
-
1
utf8 = [226].pack('U*')
-
1
refute_equal utf8.size, utf8.bytesize
-
1
TarFile::Writer.new.write('hello.txt', utf8)
-
1
assert does_not_throw=true
-
end
-
-
# - - - - - - - - - - - - - - - - - -
-
-
1
test '366', 'empty file round-trip' do
-
1
writer = TarFile::Writer.new
-
1
filename = 'greeting.txt'
-
1
writer.write(filename, '')
-
1
read = TarFile::Reader.new(writer.tar_file).files[filename]
-
1
assert_equal '', read
-
end
-
-
end
-
# frozen_string_literal: true
-
1
require_relative 'test_base'
-
1
require_source 'tgz'
-
-
1
class ServerTgzTest < TestBase
-
-
1
def self.id58_prefix
-
8
'e51'
-
end
-
-
# - - - - - - - - - - - - - - - - - -
-
-
1
test 'H3s', %w( simple tgz round-trip ) do
-
1
files_in = {
-
'hello.txt' => 'greetings earthlings...',
-
'hiker.c' => '#include <stdio.h>'
-
}
-
1
assert_equal files_in, TGZ.files(TGZ.of(files_in))
-
end
-
-
# - - - - - - - - - - - - - - - - - -
-
-
1
test 'H4s', %w( simple tgz round-trip of nothing ) do
-
1
assert_equal({}, TGZ.files(TGZ.of({})))
-
end
-
-
end
-
# frozen_string_literal: true
-
1
require_relative 'test_base'
-
1
require_source 'utf8_clean'
-
-
1
class ServerUtf8CleanTest < TestBase
-
-
1
def self.id58_prefix
-
4
'3D9'
-
end
-
-
# - - - - - - - - - - - - - - - - -
-
-
1
test '7FE', %w( cleans invalid encodings ) do
-
1
bad_str = (100..1000).to_a.pack('c*').force_encoding('utf-8')
-
1
refute bad_str.valid_encoding?
-
1
good_str = Utf8.clean(bad_str)
-
1
assert good_str.valid_encoding?
-
end
-
-
end
-
1
require_relative 'id58_test_base'
-
1
require_relative 'data/display_names'
-
1
require_relative 'doubles/stdout_logger_spy'
-
1
require_relative 'doubles/process_adapter_stub'
-
1
require_relative 'doubles/rack_request_stub'
-
1
require_relative 'doubles/bash_sheller_stub'
-
1
require_relative 'doubles/traffic_light_stub'
-
1
require_relative 'doubles/synchronous_threader'
-
1
require_source 'http_proxy/languages_start_points'
-
1
require_source 'context'
-
1
require 'json'
-
-
1
class TestBase < Id58TestBase
-
-
1
def initialize(arg)
-
60
super(arg)
-
60
context(logger:StdoutLoggerSpy.new)
-
end
-
-
1
def context(options = {})
-
97
@context ||= Context.new(options)
-
end
-
-
1
def prober
-
3
context.prober
-
end
-
-
1
def puller
-
context.puller
-
end
-
-
1
def runner
-
context.runner
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
# 1. test on one OS or many
-
-
1
def self.test(id_suffix, *lines, &block)
-
60
alpine_test(id_suffix, *lines, &block)
-
end
-
-
1
def self.multi_os_test(id_suffix, *lines, &block)
-
alpine_test(id_suffix, *lines, &block)
-
#debian_test(id_suffix, *lines, &block)
-
ubuntu_test(id_suffix, *lines, &block)
-
end
-
-
# OS specific tests
-
-
1
def self.alpine_test(id_suffix, *lines, &block)
-
60
self.csharp_nunit_test(id_suffix, *lines, &block)
-
end
-
-
1
def self.debian_test(id_suffix, *lines, &block)
-
self.c_assert_test(id_suffix, *lines, &block)
-
end
-
-
1
def self.ubuntu_test(id_suffix, *lines, &block)
-
self.clang_assert_test(id_suffix, *lines, &block)
-
end
-
-
# Language-Test-Framework specific tests
-
-
1
def self.csharp_nunit_test(id_suffix, *lines, &block)
-
60
define_test(:Alpine, DisplayNames::ALPINE, id_suffix, *lines, &block)
-
end
-
-
1
def self.c_assert_test(id_suffix, *lines, &block)
-
define_test(:Debian, DisplayNames::DEBIAN, id_suffix, *lines, &block)
-
end
-
-
1
def self.clang_assert_test(id_suffix, *lines, &block)
-
define_test(:Ubuntu, DisplayNames::UBUNTU, id_suffix, *lines, &block)
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
# 2. call helpers
-
-
1
def run_cyber_dojo_sh(options = {})
-
unchanged_files = starting_files
-
-
created_files = defaulted_arg(options, :created, {})
-
created_files.keys.each do |filename|
-
info = "#{filename} is not a created_file (it already exists)"
-
refute unchanged_files.keys.include?(filename), info
-
end
-
-
changed_files = defaulted_arg(options, :changed, {})
-
changed_files.keys.each do |filename|
-
info = "#{filename} is not a changed_file (it does not already exist)"
-
assert unchanged_files.keys.include?(filename), info
-
unchanged_files.delete(filename)
-
end
-
-
files = [ *unchanged_files, *changed_files, *created_files ].to_h
-
manifest = {
-
'image_name' => defaulted_arg(options, :image_name, image_name),
-
'max_seconds' => defaulted_arg(options, :max_seconds, max_seconds)
-
}
-
-
#p "id:#{id}"
-
#p "files:#{JSON.pretty_generate(files)}:"
-
#p "manifest:#{JSON.pretty_generate(manifest)}:"
-
-
@run_result = runner.run_cyber_dojo_sh(
-
id:id,
-
files:files,
-
manifest:manifest
-
)
-
-
#p "@run_result:#{JSON.pretty_generate(@run_result)}:"
-
-
nil
-
end
-
-
1
def defaulted_arg(named_args, arg_name, arg_default)
-
named_args.key?(arg_name) ? named_args[arg_name] : arg_default
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
# 3. call arguments
-
-
1
def id
-
id58[0..5]
-
end
-
-
1
def starting_files
-
manifest['visible_files'].each.with_object({}) do |(filename,file),memo|
-
memo[filename] = file['content']
-
end
-
end
-
-
1
def image_name
-
manifest['image_name']
-
end
-
-
1
def max_seconds
-
manifest['max_seconds']
-
end
-
-
1
def manifest
-
@manifest ||= languages_start_points.manifest(display_name)
-
end
-
-
1
def languages_start_points
-
3
::HttpProxy::LanguagesStartPoints.new
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
# 4. call results
-
-
1
def stdout; run_result['stdout']['content']; end
-
1
def stderr; run_result['stderr']['content']; end
-
-
1
def outcome; run_result['outcome']; end
-
-
1
def timed_out?; outcome === 'timed_out'; end
-
1
def pulling? ; outcome === 'pulling' ; end
-
1
def faulty? ; outcome === 'faulty' ; end
-
1
def red? ; outcome === 'red' ; end
-
1
def amber? ; outcome === 'amber' ; end
-
1
def green? ; outcome === 'green' ; end
-
-
1
def created; run_result['created']; end
-
1
def deleted; run_result['deleted']; end
-
1
def changed; run_result['changed']; end
-
-
1
def pretty_result(tag)
-
[ JSON.pretty_generate(run_result),
-
"CONTEXT:#{tag}:"
-
].join("\n")
-
end
-
-
1
attr_reader :run_result
-
-
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
# 5. custom asserts
-
-
1
def assert_sss(script)
-
assert_cyber_dojo_sh(script, traffic_light:TrafficLightStub::red)
-
end
-
-
1
def assert_cyber_dojo_sh(script, options = {})
-
options[:changed] = { 'cyber-dojo.sh' => script }
-
run_cyber_dojo_sh(options)
-
refute timed_out?
-
stdout
-
end
-
-
1
def assert_created(expected)
-
assert_hash_equal expected, created, :created
-
end
-
-
1
def assert_deleted(expected)
-
assert_equal expected, deleted, :deleted
-
end
-
-
1
def assert_changed(expected)
-
assert_hash_equal expected, changed, :changed
-
end
-
-
1
def assert_hash_equal(expected, actual, context)
-
assert_equal expected.keys.sort, actual.keys.sort, pretty_result(context)
-
expected.keys.each do |key|
-
assert_equal expected[key], actual[key], pretty_result("#{context}-#{key}")
-
end
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
# 6. misc helpers
-
-
1
def logged?(message)
-
4
log.include?(message)
-
end
-
-
1
def log
-
12
context.logger.logged
-
end
-
-
1
def uid
-
41966
-
end
-
-
1
def group
-
'sandbox'
-
end
-
-
1
def intact(content)
-
7
{ 'content' => content, 'truncated' => false }
-
end
-
-
1
def stat_cmd
-
# Works on Alpine, Debain, Ubuntu
-
'stat -c "%n %A %u %G %s" *'
-
# hiker.h -rw-r--r-- 40045 cyber-dojo 136
-
# | | | | |
-
# filename permissions uid group size
-
# 0 1 2 3 4
-
# %n %A %u %G %s
-
end
-
-
1
def assert_sha(sha)
-
2
assert sha.is_a?(String), :class
-
2
assert_equal 40, sha.size, :size
-
2
sha.each_char do |ch|
-
80
assert is_lo_hex?(ch), ch
-
end
-
end
-
-
1
def is_lo_hex?(ch)
-
80
'0123456789abcdef'.include?(ch)
-
end
-
-
end
-
# frozen_string_literal: true
-
#--
-
# benchmark.rb - a performance benchmarking library
-
#
-
# $Id$
-
#
-
# Created by Gotoken (gotoken@notwork.org).
-
#
-
# Documentation by Gotoken (original RD), Lyle Johnson (RDoc conversion), and
-
# Gavin Sinclair (editing).
-
#++
-
#
-
# == Overview
-
#
-
# The Benchmark module provides methods for benchmarking Ruby code, giving
-
# detailed reports on the time taken for each task.
-
#
-
-
# The Benchmark module provides methods to measure and report the time
-
# used to execute Ruby code.
-
#
-
# * Measure the time to construct the string given by the expression
-
# <code>"a"*1_000_000_000</code>:
-
#
-
# require 'benchmark'
-
#
-
# puts Benchmark.measure { "a"*1_000_000_000 }
-
#
-
# On my machine (OSX 10.8.3 on i5 1.7 GHz) this generates:
-
#
-
# 0.350000 0.400000 0.750000 ( 0.835234)
-
#
-
# This report shows the user CPU time, system CPU time, the sum of
-
# the user and system CPU times, and the elapsed real time. The unit
-
# of time is seconds.
-
#
-
# * Do some experiments sequentially using the #bm method:
-
#
-
# require 'benchmark'
-
#
-
# n = 5000000
-
# Benchmark.bm do |x|
-
# x.report { for i in 1..n; a = "1"; end }
-
# x.report { n.times do ; a = "1"; end }
-
# x.report { 1.upto(n) do ; a = "1"; end }
-
# end
-
#
-
# The result:
-
#
-
# user system total real
-
# 1.010000 0.000000 1.010000 ( 1.014479)
-
# 1.000000 0.000000 1.000000 ( 0.998261)
-
# 0.980000 0.000000 0.980000 ( 0.981335)
-
#
-
# * Continuing the previous example, put a label in each report:
-
#
-
# require 'benchmark'
-
#
-
# n = 5000000
-
# Benchmark.bm(7) do |x|
-
# x.report("for:") { for i in 1..n; a = "1"; end }
-
# x.report("times:") { n.times do ; a = "1"; end }
-
# x.report("upto:") { 1.upto(n) do ; a = "1"; end }
-
# end
-
#
-
# The result:
-
#
-
# user system total real
-
# for: 1.010000 0.000000 1.010000 ( 1.015688)
-
# times: 1.000000 0.000000 1.000000 ( 1.003611)
-
# upto: 1.030000 0.000000 1.030000 ( 1.028098)
-
#
-
# * The times for some benchmarks depend on the order in which items
-
# are run. These differences are due to the cost of memory
-
# allocation and garbage collection. To avoid these discrepancies,
-
# the #bmbm method is provided. For example, to compare ways to
-
# sort an array of floats:
-
#
-
# require 'benchmark'
-
#
-
# array = (1..1000000).map { rand }
-
#
-
# Benchmark.bmbm do |x|
-
# x.report("sort!") { array.dup.sort! }
-
# x.report("sort") { array.dup.sort }
-
# end
-
#
-
# The result:
-
#
-
# Rehearsal -----------------------------------------
-
# sort! 1.490000 0.010000 1.500000 ( 1.490520)
-
# sort 1.460000 0.000000 1.460000 ( 1.463025)
-
# -------------------------------- total: 2.960000sec
-
#
-
# user system total real
-
# sort! 1.460000 0.000000 1.460000 ( 1.460465)
-
# sort 1.450000 0.010000 1.460000 ( 1.448327)
-
#
-
# * Report statistics of sequential experiments with unique labels,
-
# using the #benchmark method:
-
#
-
# require 'benchmark'
-
# include Benchmark # we need the CAPTION and FORMAT constants
-
#
-
# n = 5000000
-
# Benchmark.benchmark(CAPTION, 7, FORMAT, ">total:", ">avg:") do |x|
-
# tf = x.report("for:") { for i in 1..n; a = "1"; end }
-
# tt = x.report("times:") { n.times do ; a = "1"; end }
-
# tu = x.report("upto:") { 1.upto(n) do ; a = "1"; end }
-
# [tf+tt+tu, (tf+tt+tu)/3]
-
# end
-
#
-
# The result:
-
#
-
# user system total real
-
# for: 0.950000 0.000000 0.950000 ( 0.952039)
-
# times: 0.980000 0.000000 0.980000 ( 0.984938)
-
# upto: 0.950000 0.000000 0.950000 ( 0.946787)
-
# >total: 2.880000 0.000000 2.880000 ( 2.883764)
-
# >avg: 0.960000 0.000000 0.960000 ( 0.961255)
-
-
1
module Benchmark
-
-
1
BENCHMARK_VERSION = "2002-04-25" # :nodoc:
-
-
# Invokes the block with a Benchmark::Report object, which
-
# may be used to collect and report on the results of individual
-
# benchmark tests. Reserves +label_width+ leading spaces for
-
# labels on each line. Prints +caption+ at the top of the
-
# report, and uses +format+ to format each line.
-
# Returns an array of Benchmark::Tms objects.
-
#
-
# If the block returns an array of
-
# Benchmark::Tms objects, these will be used to format
-
# additional lines of output. If +labels+ parameter are
-
# given, these are used to label these extra lines.
-
#
-
# _Note_: Other methods provide a simpler interface to this one, and are
-
# suitable for nearly all benchmarking requirements. See the examples in
-
# Benchmark, and the #bm and #bmbm methods.
-
#
-
# Example:
-
#
-
# require 'benchmark'
-
# include Benchmark # we need the CAPTION and FORMAT constants
-
#
-
# n = 5000000
-
# Benchmark.benchmark(CAPTION, 7, FORMAT, ">total:", ">avg:") do |x|
-
# tf = x.report("for:") { for i in 1..n; a = "1"; end }
-
# tt = x.report("times:") { n.times do ; a = "1"; end }
-
# tu = x.report("upto:") { 1.upto(n) do ; a = "1"; end }
-
# [tf+tt+tu, (tf+tt+tu)/3]
-
# end
-
#
-
# Generates:
-
#
-
# user system total real
-
# for: 0.970000 0.000000 0.970000 ( 0.970493)
-
# times: 0.990000 0.000000 0.990000 ( 0.989542)
-
# upto: 0.970000 0.000000 0.970000 ( 0.972854)
-
# >total: 2.930000 0.000000 2.930000 ( 2.932889)
-
# >avg: 0.976667 0.000000 0.976667 ( 0.977630)
-
#
-
-
1
def benchmark(caption = "", label_width = nil, format = nil, *labels) # :yield: report
-
sync = STDOUT.sync
-
STDOUT.sync = true
-
label_width ||= 0
-
label_width += 1
-
format ||= FORMAT
-
print ' '*label_width + caption unless caption.empty?
-
report = Report.new(label_width, format)
-
results = yield(report)
-
Array === results and results.grep(Tms).each {|t|
-
print((labels.shift || t.label || "").ljust(label_width), t.format(format))
-
}
-
report.list
-
ensure
-
STDOUT.sync = sync unless sync.nil?
-
end
-
-
-
# A simple interface to the #benchmark method, #bm generates sequential
-
# reports with labels. +label_width+ and +labels+ parameters have the same
-
# meaning as for #benchmark.
-
#
-
# require 'benchmark'
-
#
-
# n = 5000000
-
# Benchmark.bm(7) do |x|
-
# x.report("for:") { for i in 1..n; a = "1"; end }
-
# x.report("times:") { n.times do ; a = "1"; end }
-
# x.report("upto:") { 1.upto(n) do ; a = "1"; end }
-
# end
-
#
-
# Generates:
-
#
-
# user system total real
-
# for: 0.960000 0.000000 0.960000 ( 0.957966)
-
# times: 0.960000 0.000000 0.960000 ( 0.960423)
-
# upto: 0.950000 0.000000 0.950000 ( 0.954864)
-
#
-
-
1
def bm(label_width = 0, *labels, &blk) # :yield: report
-
benchmark(CAPTION, label_width, FORMAT, *labels, &blk)
-
end
-
-
-
# Sometimes benchmark results are skewed because code executed
-
# earlier encounters different garbage collection overheads than
-
# that run later. #bmbm attempts to minimize this effect by running
-
# the tests twice, the first time as a rehearsal in order to get the
-
# runtime environment stable, the second time for
-
# real. GC.start is executed before the start of each of
-
# the real timings; the cost of this is not included in the
-
# timings. In reality, though, there's only so much that #bmbm can
-
# do, and the results are not guaranteed to be isolated from garbage
-
# collection and other effects.
-
#
-
# Because #bmbm takes two passes through the tests, it can
-
# calculate the required label width.
-
#
-
# require 'benchmark'
-
#
-
# array = (1..1000000).map { rand }
-
#
-
# Benchmark.bmbm do |x|
-
# x.report("sort!") { array.dup.sort! }
-
# x.report("sort") { array.dup.sort }
-
# end
-
#
-
# Generates:
-
#
-
# Rehearsal -----------------------------------------
-
# sort! 1.440000 0.010000 1.450000 ( 1.446833)
-
# sort 1.440000 0.000000 1.440000 ( 1.448257)
-
# -------------------------------- total: 2.890000sec
-
#
-
# user system total real
-
# sort! 1.460000 0.000000 1.460000 ( 1.458065)
-
# sort 1.450000 0.000000 1.450000 ( 1.455963)
-
#
-
# #bmbm yields a Benchmark::Job object and returns an array of
-
# Benchmark::Tms objects.
-
#
-
1
def bmbm(width = 0) # :yield: job
-
job = Job.new(width)
-
yield(job)
-
width = job.width + 1
-
sync = STDOUT.sync
-
STDOUT.sync = true
-
-
# rehearsal
-
puts 'Rehearsal '.ljust(width+CAPTION.length,'-')
-
ets = job.list.inject(Tms.new) { |sum,(label,item)|
-
print label.ljust(width)
-
res = Benchmark.measure(&item)
-
print res.format
-
sum + res
-
}.format("total: %tsec")
-
print " #{ets}\n\n".rjust(width+CAPTION.length+2,'-')
-
-
# take
-
print ' '*width + CAPTION
-
job.list.map { |label,item|
-
GC.start
-
print label.ljust(width)
-
Benchmark.measure(label, &item).tap { |res| print res }
-
}
-
ensure
-
STDOUT.sync = sync unless sync.nil?
-
end
-
-
#
-
# Returns the time used to execute the given block as a
-
# Benchmark::Tms object. Takes +label+ option.
-
#
-
# require 'benchmark'
-
#
-
# n = 1000000
-
#
-
# time = Benchmark.measure do
-
# n.times { a = "1" }
-
# end
-
# puts time
-
#
-
# Generates:
-
#
-
# 0.220000 0.000000 0.220000 ( 0.227313)
-
#
-
1
def measure(label = "") # :yield:
-
t0, r0 = Process.times, Process.clock_gettime(Process::CLOCK_MONOTONIC)
-
yield
-
t1, r1 = Process.times, Process.clock_gettime(Process::CLOCK_MONOTONIC)
-
Benchmark::Tms.new(t1.utime - t0.utime,
-
t1.stime - t0.stime,
-
t1.cutime - t0.cutime,
-
t1.cstime - t0.cstime,
-
r1 - r0,
-
label)
-
end
-
-
#
-
# Returns the elapsed real time used to execute the given block.
-
#
-
1
def realtime # :yield:
-
r0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
-
yield
-
Process.clock_gettime(Process::CLOCK_MONOTONIC) - r0
-
end
-
-
1
module_function :benchmark, :measure, :realtime, :bm, :bmbm
-
-
#
-
# A Job is a sequence of labelled blocks to be processed by the
-
# Benchmark.bmbm method. It is of little direct interest to the user.
-
#
-
1
class Job # :nodoc:
-
#
-
# Returns an initialized Job instance.
-
# Usually, one doesn't call this method directly, as new
-
# Job objects are created by the #bmbm method.
-
# +width+ is a initial value for the label offset used in formatting;
-
# the #bmbm method passes its +width+ argument to this constructor.
-
#
-
1
def initialize(width)
-
@width = width
-
@list = []
-
end
-
-
#
-
# Registers the given label and block pair in the job list.
-
#
-
1
def item(label = "", &blk) # :yield:
-
raise ArgumentError, "no block" unless block_given?
-
label = label.to_s
-
w = label.length
-
@width = w if @width < w
-
@list << [label, blk]
-
self
-
end
-
-
1
alias report item
-
-
# An array of 2-element arrays, consisting of label and block pairs.
-
1
attr_reader :list
-
-
# Length of the widest label in the #list.
-
1
attr_reader :width
-
end
-
-
#
-
# This class is used by the Benchmark.benchmark and Benchmark.bm methods.
-
# It is of little direct interest to the user.
-
#
-
1
class Report # :nodoc:
-
#
-
# Returns an initialized Report instance.
-
# Usually, one doesn't call this method directly, as new
-
# Report objects are created by the #benchmark and #bm methods.
-
# +width+ and +format+ are the label offset and
-
# format string used by Tms#format.
-
#
-
1
def initialize(width = 0, format = nil)
-
@width, @format, @list = width, format, []
-
end
-
-
#
-
# Prints the +label+ and measured time for the block,
-
# formatted by +format+. See Tms#format for the
-
# formatting rules.
-
#
-
1
def item(label = "", *format, &blk) # :yield:
-
print label.to_s.ljust(@width)
-
@list << res = Benchmark.measure(label, &blk)
-
print res.format(@format, *format)
-
res
-
end
-
-
1
alias report item
-
-
# An array of Benchmark::Tms objects representing each item.
-
1
attr_reader :list
-
end
-
-
-
-
#
-
# A data object, representing the times associated with a benchmark
-
# measurement.
-
#
-
1
class Tms
-
-
# Default caption, see also Benchmark::CAPTION
-
1
CAPTION = " user system total real\n"
-
-
# Default format string, see also Benchmark::FORMAT
-
1
FORMAT = "%10.6u %10.6y %10.6t %10.6r\n"
-
-
# User CPU time
-
1
attr_reader :utime
-
-
# System CPU time
-
1
attr_reader :stime
-
-
# User CPU time of children
-
1
attr_reader :cutime
-
-
# System CPU time of children
-
1
attr_reader :cstime
-
-
# Elapsed real time
-
1
attr_reader :real
-
-
# Total time, that is +utime+ + +stime+ + +cutime+ + +cstime+
-
1
attr_reader :total
-
-
# Label
-
1
attr_reader :label
-
-
#
-
# Returns an initialized Tms object which has
-
# +utime+ as the user CPU time, +stime+ as the system CPU time,
-
# +cutime+ as the children's user CPU time, +cstime+ as the children's
-
# system CPU time, +real+ as the elapsed real time and +label+ as the label.
-
#
-
1
def initialize(utime = 0.0, stime = 0.0, cutime = 0.0, cstime = 0.0, real = 0.0, label = nil)
-
@utime, @stime, @cutime, @cstime, @real, @label = utime, stime, cutime, cstime, real, label.to_s
-
@total = @utime + @stime + @cutime + @cstime
-
end
-
-
#
-
# Returns a new Tms object whose times are the sum of the times for this
-
# Tms object, plus the time required to execute the code block (+blk+).
-
#
-
1
def add(&blk) # :yield:
-
self + Benchmark.measure(&blk)
-
end
-
-
#
-
# An in-place version of #add.
-
# Changes the times of this Tms object by making it the sum of the times
-
# for this Tms object, plus the time required to execute
-
# the code block (+blk+).
-
#
-
1
def add!(&blk)
-
t = Benchmark.measure(&blk)
-
@utime = utime + t.utime
-
@stime = stime + t.stime
-
@cutime = cutime + t.cutime
-
@cstime = cstime + t.cstime
-
@real = real + t.real
-
self
-
end
-
-
#
-
# Returns a new Tms object obtained by memberwise summation
-
# of the individual times for this Tms object with those of the +other+
-
# Tms object.
-
# This method and #/() are useful for taking statistics.
-
#
-
1
def +(other); memberwise(:+, other) end
-
-
#
-
# Returns a new Tms object obtained by memberwise subtraction
-
# of the individual times for the +other+ Tms object from those of this
-
# Tms object.
-
#
-
1
def -(other); memberwise(:-, other) end
-
-
#
-
# Returns a new Tms object obtained by memberwise multiplication
-
# of the individual times for this Tms object by +x+.
-
#
-
1
def *(x); memberwise(:*, x) end
-
-
#
-
# Returns a new Tms object obtained by memberwise division
-
# of the individual times for this Tms object by +x+.
-
# This method and #+() are useful for taking statistics.
-
#
-
1
def /(x); memberwise(:/, x) end
-
-
#
-
# Returns the contents of this Tms object as
-
# a formatted string, according to a +format+ string
-
# like that passed to Kernel.format. In addition, #format
-
# accepts the following extensions:
-
#
-
# <tt>%u</tt>:: Replaced by the user CPU time, as reported by Tms#utime.
-
# <tt>%y</tt>:: Replaced by the system CPU time, as reported by #stime (Mnemonic: y of "s*y*stem")
-
# <tt>%U</tt>:: Replaced by the children's user CPU time, as reported by Tms#cutime
-
# <tt>%Y</tt>:: Replaced by the children's system CPU time, as reported by Tms#cstime
-
# <tt>%t</tt>:: Replaced by the total CPU time, as reported by Tms#total
-
# <tt>%r</tt>:: Replaced by the elapsed real time, as reported by Tms#real
-
# <tt>%n</tt>:: Replaced by the label string, as reported by Tms#label (Mnemonic: n of "*n*ame")
-
#
-
# If +format+ is not given, FORMAT is used as default value, detailing the
-
# user, system and real elapsed time.
-
#
-
1
def format(format = nil, *args)
-
str = (format || FORMAT).dup
-
str.gsub!(/(%[-+.\d]*)n/) { "#{$1}s" % label }
-
str.gsub!(/(%[-+.\d]*)u/) { "#{$1}f" % utime }
-
str.gsub!(/(%[-+.\d]*)y/) { "#{$1}f" % stime }
-
str.gsub!(/(%[-+.\d]*)U/) { "#{$1}f" % cutime }
-
str.gsub!(/(%[-+.\d]*)Y/) { "#{$1}f" % cstime }
-
str.gsub!(/(%[-+.\d]*)t/) { "#{$1}f" % total }
-
str.gsub!(/(%[-+.\d]*)r/) { "(#{$1}f)" % real }
-
format ? str % args : str
-
end
-
-
#
-
# Same as #format.
-
#
-
1
def to_s
-
format
-
end
-
-
#
-
# Returns a new 6-element array, consisting of the
-
# label, user CPU time, system CPU time, children's
-
# user CPU time, children's system CPU time and elapsed
-
# real time.
-
#
-
1
def to_a
-
[@label, @utime, @stime, @cutime, @cstime, @real]
-
end
-
-
1
protected
-
-
#
-
# Returns a new Tms object obtained by memberwise operation +op+
-
# of the individual times for this Tms object with those of the other
-
# Tms object (+x+).
-
#
-
# +op+ can be a mathematical operation such as <tt>+</tt>, <tt>-</tt>,
-
# <tt>*</tt>, <tt>/</tt>
-
#
-
1
def memberwise(op, x)
-
case x
-
when Benchmark::Tms
-
Benchmark::Tms.new(utime.__send__(op, x.utime),
-
stime.__send__(op, x.stime),
-
cutime.__send__(op, x.cutime),
-
cstime.__send__(op, x.cstime),
-
real.__send__(op, x.real)
-
)
-
else
-
Benchmark::Tms.new(utime.__send__(op, x),
-
stime.__send__(op, x),
-
cutime.__send__(op, x),
-
cstime.__send__(op, x),
-
real.__send__(op, x)
-
)
-
end
-
end
-
end
-
-
# The default caption string (heading above the output times).
-
1
CAPTION = Benchmark::Tms::CAPTION
-
-
# The default format string used to display times. See also Benchmark::Tms#format.
-
1
FORMAT = Benchmark::Tms::FORMAT
-
end
-
# frozen_string_literal: true
-
# = delegate -- Support for the Delegation Pattern
-
#
-
# Documentation by James Edward Gray II and Gavin Sinclair
-
-
##
-
# This library provides three different ways to delegate method calls to an
-
# object. The easiest to use is SimpleDelegator. Pass an object to the
-
# constructor and all methods supported by the object will be delegated. This
-
# object can be changed later.
-
#
-
# Going a step further, the top level DelegateClass method allows you to easily
-
# setup delegation through class inheritance. This is considerably more
-
# flexible and thus probably the most common use for this library.
-
#
-
# Finally, if you need full control over the delegation scheme, you can inherit
-
# from the abstract class Delegator and customize as needed. (If you find
-
# yourself needing this control, have a look at Forwardable which is also in
-
# the standard library. It may suit your needs better.)
-
#
-
# SimpleDelegator's implementation serves as a nice example of the use of
-
# Delegator:
-
#
-
# class SimpleDelegator < Delegator
-
# def __getobj__
-
# @delegate_sd_obj # return object we are delegating to, required
-
# end
-
#
-
# def __setobj__(obj)
-
# @delegate_sd_obj = obj # change delegation object,
-
# # a feature we're providing
-
# end
-
# end
-
#
-
# == Notes
-
#
-
# Be advised, RDoc will not detect delegated methods.
-
#
-
1
class Delegator < BasicObject
-
1
kernel = ::Kernel.dup
-
1
kernel.class_eval do
-
1
alias __raise__ raise
-
1
[:to_s, :inspect, :=~, :!~, :===, :<=>, :hash].each do |m|
-
7
undef_method m
-
end
-
1
private_instance_methods.each do |m|
-
73
if /\Ablock_given\?\z|\Aiterator\?\z|\A__.*__\z/ =~ m
-
6
next
-
end
-
67
undef_method m
-
end
-
end
-
1
include kernel
-
-
# :stopdoc:
-
1
def self.const_missing(n)
-
::Object.const_get(n)
-
end
-
# :startdoc:
-
-
##
-
# :method: raise
-
# Use #__raise__ if your Delegator does not have a object to delegate the
-
# #raise method call.
-
#
-
-
#
-
# Pass in the _obj_ to delegate method calls to. All methods supported by
-
# _obj_ will be delegated to.
-
#
-
1
def initialize(obj)
-
__setobj__(obj)
-
end
-
-
#
-
# Handles the magic of delegation through \_\_getobj\_\_.
-
#
-
1
ruby2_keywords def method_missing(m, *args, &block)
-
r = true
-
target = self.__getobj__ {r = false}
-
-
if r && target_respond_to?(target, m, false)
-
target.__send__(m, *args, &block)
-
elsif ::Kernel.method_defined?(m) || ::Kernel.private_method_defined?(m)
-
::Kernel.instance_method(m).bind_call(self, *args, &block)
-
else
-
super(m, *args, &block)
-
end
-
end
-
-
#
-
# Checks for a method provided by this the delegate object by forwarding the
-
# call through \_\_getobj\_\_.
-
#
-
1
def respond_to_missing?(m, include_private)
-
r = true
-
target = self.__getobj__ {r = false}
-
r &&= target_respond_to?(target, m, include_private)
-
if r && include_private && !target_respond_to?(target, m, false)
-
warn "delegator does not forward private method \##{m}", uplevel: 3
-
return false
-
end
-
r
-
end
-
-
1
KERNEL_RESPOND_TO = ::Kernel.instance_method(:respond_to?)
-
1
private_constant :KERNEL_RESPOND_TO
-
-
# Handle BasicObject instances
-
1
private def target_respond_to?(target, m, include_private)
-
case target
-
when Object
-
target.respond_to?(m, include_private)
-
else
-
if KERNEL_RESPOND_TO.bind_call(target, :respond_to?)
-
target.respond_to?(m, include_private)
-
else
-
KERNEL_RESPOND_TO.bind_call(target, m, include_private)
-
end
-
end
-
end
-
-
#
-
# Returns the methods available to this delegate object as the union
-
# of this object's and \_\_getobj\_\_ methods.
-
#
-
1
def methods(all=true)
-
__getobj__.methods(all) | super
-
end
-
-
#
-
# Returns the methods available to this delegate object as the union
-
# of this object's and \_\_getobj\_\_ public methods.
-
#
-
1
def public_methods(all=true)
-
__getobj__.public_methods(all) | super
-
end
-
-
#
-
# Returns the methods available to this delegate object as the union
-
# of this object's and \_\_getobj\_\_ protected methods.
-
#
-
1
def protected_methods(all=true)
-
__getobj__.protected_methods(all) | super
-
end
-
-
# Note: no need to specialize private_methods, since they are not forwarded
-
-
#
-
# Returns true if two objects are considered of equal value.
-
#
-
1
def ==(obj)
-
return true if obj.equal?(self)
-
self.__getobj__ == obj
-
end
-
-
#
-
# Returns true if two objects are not considered of equal value.
-
#
-
1
def !=(obj)
-
return false if obj.equal?(self)
-
__getobj__ != obj
-
end
-
-
#
-
# Returns true if two objects are considered of equal value.
-
#
-
1
def eql?(obj)
-
return true if obj.equal?(self)
-
obj.eql?(__getobj__)
-
end
-
-
#
-
# Delegates ! to the \_\_getobj\_\_
-
#
-
1
def !
-
!__getobj__
-
end
-
-
#
-
# This method must be overridden by subclasses and should return the object
-
# method calls are being delegated to.
-
#
-
1
def __getobj__
-
__raise__ ::NotImplementedError, "need to define `__getobj__'"
-
end
-
-
#
-
# This method must be overridden by subclasses and change the object delegate
-
# to _obj_.
-
#
-
1
def __setobj__(obj)
-
__raise__ ::NotImplementedError, "need to define `__setobj__'"
-
end
-
-
#
-
# Serialization support for the object returned by \_\_getobj\_\_.
-
#
-
1
def marshal_dump
-
ivars = instance_variables.reject {|var| /\A@delegate_/ =~ var}
-
[
-
:__v2__,
-
ivars, ivars.map {|var| instance_variable_get(var)},
-
__getobj__
-
]
-
end
-
-
#
-
# Reinitializes delegation from a serialized object.
-
#
-
1
def marshal_load(data)
-
version, vars, values, obj = data
-
if version == :__v2__
-
vars.each_with_index {|var, i| instance_variable_set(var, values[i])}
-
__setobj__(obj)
-
else
-
__setobj__(data)
-
end
-
end
-
-
1
def initialize_clone(obj) # :nodoc:
-
self.__setobj__(obj.__getobj__.clone)
-
end
-
1
def initialize_dup(obj) # :nodoc:
-
self.__setobj__(obj.__getobj__.dup)
-
end
-
1
private :initialize_clone, :initialize_dup
-
-
##
-
# :method: freeze
-
# Freeze both the object returned by \_\_getobj\_\_ and self.
-
#
-
1
def freeze
-
__getobj__.freeze
-
super()
-
end
-
-
1
@delegator_api = self.public_instance_methods
-
1
def self.public_api # :nodoc:
-
1
@delegator_api
-
end
-
end
-
-
##
-
# A concrete implementation of Delegator, this class provides the means to
-
# delegate all supported method calls to the object passed into the constructor
-
# and even to change the object being delegated to at a later time with
-
# #__setobj__.
-
#
-
# class User
-
# def born_on
-
# Date.new(1989, 9, 10)
-
# end
-
# end
-
#
-
# class UserDecorator < SimpleDelegator
-
# def birth_year
-
# born_on.year
-
# end
-
# end
-
#
-
# decorated_user = UserDecorator.new(User.new)
-
# decorated_user.birth_year #=> 1989
-
# decorated_user.__getobj__ #=> #<User: ...>
-
#
-
# A SimpleDelegator instance can take advantage of the fact that SimpleDelegator
-
# is a subclass of +Delegator+ to call <tt>super</tt> to have methods called on
-
# the object being delegated to.
-
#
-
# class SuperArray < SimpleDelegator
-
# def [](*args)
-
# super + 1
-
# end
-
# end
-
#
-
# SuperArray.new([1])[0] #=> 2
-
#
-
# Here's a simple example that takes advantage of the fact that
-
# SimpleDelegator's delegation object can be changed at any time.
-
#
-
# class Stats
-
# def initialize
-
# @source = SimpleDelegator.new([])
-
# end
-
#
-
# def stats(records)
-
# @source.__setobj__(records)
-
#
-
# "Elements: #{@source.size}\n" +
-
# " Non-Nil: #{@source.compact.size}\n" +
-
# " Unique: #{@source.uniq.size}\n"
-
# end
-
# end
-
#
-
# s = Stats.new
-
# puts s.stats(%w{James Edward Gray II})
-
# puts
-
# puts s.stats([1, 2, 3, nil, 4, 5, 1, 2])
-
#
-
# Prints:
-
#
-
# Elements: 4
-
# Non-Nil: 4
-
# Unique: 4
-
#
-
# Elements: 8
-
# Non-Nil: 7
-
# Unique: 6
-
#
-
1
class SimpleDelegator < Delegator
-
# Returns the current object method calls are being delegated to.
-
1
def __getobj__
-
unless defined?(@delegate_sd_obj)
-
return yield if block_given?
-
__raise__ ::ArgumentError, "not delegated"
-
end
-
@delegate_sd_obj
-
end
-
-
#
-
# Changes the delegate object to _obj_.
-
#
-
# It's important to note that this does *not* cause SimpleDelegator's methods
-
# to change. Because of this, you probably only want to change delegation
-
# to objects of the same type as the original delegate.
-
#
-
# Here's an example of changing the delegation object.
-
#
-
# names = SimpleDelegator.new(%w{James Edward Gray II})
-
# puts names[1] # => Edward
-
# names.__setobj__(%w{Gavin Sinclair})
-
# puts names[1] # => Sinclair
-
#
-
1
def __setobj__(obj)
-
__raise__ ::ArgumentError, "cannot delegate to self" if self.equal?(obj)
-
@delegate_sd_obj = obj
-
end
-
end
-
-
1
def Delegator.delegating_block(mid) # :nodoc:
-
148
lambda do |*args, &block|
-
target = self.__getobj__
-
target.__send__(mid, *args, &block)
-
end.ruby2_keywords
-
end
-
-
#
-
# The primary interface to this library. Use to setup delegation when defining
-
# your class.
-
#
-
# class MyClass < DelegateClass(ClassToDelegateTo) # Step 1
-
# def initialize
-
# super(obj_of_ClassToDelegateTo) # Step 2
-
# end
-
# end
-
#
-
# or:
-
#
-
# MyClass = DelegateClass(ClassToDelegateTo) do # Step 1
-
# def initialize
-
# super(obj_of_ClassToDelegateTo) # Step 2
-
# end
-
# end
-
#
-
# Here's a sample of use from Tempfile which is really a File object with a
-
# few special rules about storage location and when the File should be
-
# deleted. That makes for an almost textbook perfect example of how to use
-
# delegation.
-
#
-
# class Tempfile < DelegateClass(File)
-
# # constant and class member data initialization...
-
#
-
# def initialize(basename, tmpdir=Dir::tmpdir)
-
# # build up file path/name in var tmpname...
-
#
-
# @tmpfile = File.open(tmpname, File::RDWR|File::CREAT|File::EXCL, 0600)
-
#
-
# # ...
-
#
-
# super(@tmpfile)
-
#
-
# # below this point, all methods of File are supported...
-
# end
-
#
-
# # ...
-
# end
-
#
-
1
def DelegateClass(superclass, &block)
-
1
klass = Class.new(Delegator)
-
1
ignores = [*::Delegator.public_api, :to_s, :inspect, :=~, :!~, :===]
-
1
protected_instance_methods = superclass.protected_instance_methods
-
1
protected_instance_methods -= ignores
-
1
public_instance_methods = superclass.public_instance_methods
-
1
public_instance_methods -= ignores
-
1
klass.module_eval do
-
1
def __getobj__ # :nodoc:
-
unless defined?(@delegate_dc_obj)
-
return yield if block_given?
-
__raise__ ::ArgumentError, "not delegated"
-
end
-
@delegate_dc_obj
-
end
-
1
def __setobj__(obj) # :nodoc:
-
__raise__ ::ArgumentError, "cannot delegate to self" if self.equal?(obj)
-
@delegate_dc_obj = obj
-
end
-
1
protected_instance_methods.each do |method|
-
define_method(method, Delegator.delegating_block(method))
-
protected method
-
end
-
1
public_instance_methods.each do |method|
-
148
define_method(method, Delegator.delegating_block(method))
-
end
-
end
-
1
klass.define_singleton_method :public_instance_methods do |all=true|
-
super(all) | superclass.public_instance_methods
-
end
-
1
klass.define_singleton_method :protected_instance_methods do |all=true|
-
super(all) | superclass.protected_instance_methods
-
end
-
1
klass.module_eval(&block) if block
-
1
return klass
-
end
-
# frozen_string_literal: true
-
#
-
# ipaddr.rb - A class to manipulate an IP address
-
#
-
# Copyright (c) 2002 Hajimu UMEMOTO <ume@mahoroba.org>.
-
# Copyright (c) 2007, 2009, 2012 Akinori MUSHA <knu@iDaemons.org>.
-
# All rights reserved.
-
#
-
# You can redistribute and/or modify it under the same terms as Ruby.
-
#
-
# $Id$
-
#
-
# Contact:
-
# - Akinori MUSHA <knu@iDaemons.org> (current maintainer)
-
#
-
# TODO:
-
# - scope_id support
-
#
-
1
require 'socket'
-
-
# IPAddr provides a set of methods to manipulate an IP address. Both IPv4 and
-
# IPv6 are supported.
-
#
-
# == Example
-
#
-
# require 'ipaddr'
-
#
-
# ipaddr1 = IPAddr.new "3ffe:505:2::1"
-
#
-
# p ipaddr1 #=> #<IPAddr: IPv6:3ffe:0505:0002:0000:0000:0000:0000:0001/ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff>
-
#
-
# p ipaddr1.to_s #=> "3ffe:505:2::1"
-
#
-
# ipaddr2 = ipaddr1.mask(48) #=> #<IPAddr: IPv6:3ffe:0505:0002:0000:0000:0000:0000:0000/ffff:ffff:ffff:0000:0000:0000:0000:0000>
-
#
-
# p ipaddr2.to_s #=> "3ffe:505:2::"
-
#
-
# ipaddr3 = IPAddr.new "192.168.2.0/24"
-
#
-
# p ipaddr3 #=> #<IPAddr: IPv4:192.168.2.0/255.255.255.0>
-
-
1
class IPAddr
-
-
# 32 bit mask for IPv4
-
1
IN4MASK = 0xffffffff
-
# 128 bit mask for IPv6
-
1
IN6MASK = 0xffffffffffffffffffffffffffffffff
-
# Format string for IPv6
-
1
IN6FORMAT = (["%.4x"] * 8).join(':')
-
-
# Regexp _internally_ used for parsing IPv4 address.
-
1
RE_IPV4ADDRLIKE = %r{
-
\A
-
(\d+) \. (\d+) \. (\d+) \. (\d+)
-
\z
-
}x
-
-
# Regexp _internally_ used for parsing IPv6 address.
-
1
RE_IPV6ADDRLIKE_FULL = %r{
-
\A
-
(?:
-
(?: [\da-f]{1,4} : ){7} [\da-f]{1,4}
-
|
-
( (?: [\da-f]{1,4} : ){6} )
-
(\d+) \. (\d+) \. (\d+) \. (\d+)
-
)
-
\z
-
}xi
-
-
# Regexp _internally_ used for parsing IPv6 address.
-
1
RE_IPV6ADDRLIKE_COMPRESSED = %r{
-
\A
-
( (?: (?: [\da-f]{1,4} : )* [\da-f]{1,4} )? )
-
::
-
( (?:
-
( (?: [\da-f]{1,4} : )* )
-
(?:
-
[\da-f]{1,4}
-
|
-
(\d+) \. (\d+) \. (\d+) \. (\d+)
-
)
-
)? )
-
\z
-
}xi
-
-
# Generic IPAddr related error. Exceptions raised in this class should
-
# inherit from Error.
-
1
class Error < ArgumentError; end
-
-
# Raised when the provided IP address is an invalid address.
-
1
class InvalidAddressError < Error; end
-
-
# Raised when the address family is invalid such as an address with an
-
# unsupported family, an address with an inconsistent family, or an address
-
# who's family cannot be determined.
-
1
class AddressFamilyError < Error; end
-
-
# Raised when the address is an invalid length.
-
1
class InvalidPrefixError < InvalidAddressError; end
-
-
# Returns the address family of this IP address.
-
1
attr_reader :family
-
-
# Creates a new ipaddr containing the given network byte ordered
-
# string form of an IP address.
-
1
def self.new_ntoh(addr)
-
return new(ntop(addr))
-
end
-
-
# Convert a network byte ordered string form of an IP address into
-
# human readable form.
-
1
def self.ntop(addr)
-
case addr.size
-
when 4
-
s = addr.unpack('C4').join('.')
-
when 16
-
s = IN6FORMAT % addr.unpack('n8')
-
else
-
raise AddressFamilyError, "unsupported address family"
-
end
-
return s
-
end
-
-
# Returns a new ipaddr built by bitwise AND.
-
1
def &(other)
-
return self.clone.set(@addr & coerce_other(other).to_i)
-
end
-
-
# Returns a new ipaddr built by bitwise OR.
-
1
def |(other)
-
return self.clone.set(@addr | coerce_other(other).to_i)
-
end
-
-
# Returns a new ipaddr built by bitwise right-shift.
-
1
def >>(num)
-
return self.clone.set(@addr >> num)
-
end
-
-
# Returns a new ipaddr built by bitwise left shift.
-
1
def <<(num)
-
return self.clone.set(addr_mask(@addr << num))
-
end
-
-
# Returns a new ipaddr built by bitwise negation.
-
1
def ~
-
return self.clone.set(addr_mask(~@addr))
-
end
-
-
# Returns true if two ipaddrs are equal.
-
1
def ==(other)
-
other = coerce_other(other)
-
rescue
-
false
-
else
-
@family == other.family && @addr == other.to_i
-
end
-
-
# Returns a new ipaddr built by masking IP address with the given
-
# prefixlen/netmask. (e.g. 8, 64, "255.255.255.0", etc.)
-
1
def mask(prefixlen)
-
return self.clone.mask!(prefixlen)
-
end
-
-
# Returns true if the given ipaddr is in the range.
-
#
-
# e.g.:
-
# require 'ipaddr'
-
# net1 = IPAddr.new("192.168.2.0/24")
-
# net2 = IPAddr.new("192.168.2.100")
-
# net3 = IPAddr.new("192.168.3.0")
-
# p net1.include?(net2) #=> true
-
# p net1.include?(net3) #=> false
-
1
def include?(other)
-
other = coerce_other(other)
-
if ipv4_mapped?
-
if (@mask_addr >> 32) != 0xffffffffffffffffffffffff
-
return false
-
end
-
mask_addr = (@mask_addr & IN4MASK)
-
addr = (@addr & IN4MASK)
-
family = Socket::AF_INET
-
else
-
mask_addr = @mask_addr
-
addr = @addr
-
family = @family
-
end
-
if other.ipv4_mapped?
-
other_addr = (other.to_i & IN4MASK)
-
other_family = Socket::AF_INET
-
else
-
other_addr = other.to_i
-
other_family = other.family
-
end
-
-
if family != other_family
-
return false
-
end
-
return ((addr & mask_addr) == (other_addr & mask_addr))
-
end
-
1
alias === include?
-
-
# Returns the integer representation of the ipaddr.
-
1
def to_i
-
return @addr
-
end
-
-
# Returns a string containing the IP address representation.
-
1
def to_s
-
str = to_string
-
return str if ipv4?
-
-
str.gsub!(/\b0{1,3}([\da-f]+)\b/i, '\1')
-
loop do
-
break if str.sub!(/\A0:0:0:0:0:0:0:0\z/, '::')
-
break if str.sub!(/\b0:0:0:0:0:0:0\b/, ':')
-
break if str.sub!(/\b0:0:0:0:0:0\b/, ':')
-
break if str.sub!(/\b0:0:0:0:0\b/, ':')
-
break if str.sub!(/\b0:0:0:0\b/, ':')
-
break if str.sub!(/\b0:0:0\b/, ':')
-
break if str.sub!(/\b0:0\b/, ':')
-
break
-
end
-
str.sub!(/:{3,}/, '::')
-
-
if /\A::(ffff:)?([\da-f]{1,4}):([\da-f]{1,4})\z/i =~ str
-
str = sprintf('::%s%d.%d.%d.%d', $1, $2.hex / 256, $2.hex % 256, $3.hex / 256, $3.hex % 256)
-
end
-
-
str
-
end
-
-
# Returns a string containing the IP address representation in
-
# canonical form.
-
1
def to_string
-
return _to_string(@addr)
-
end
-
-
# Returns a network byte ordered string form of the IP address.
-
1
def hton
-
case @family
-
when Socket::AF_INET
-
return [@addr].pack('N')
-
when Socket::AF_INET6
-
return (0..7).map { |i|
-
(@addr >> (112 - 16 * i)) & 0xffff
-
}.pack('n8')
-
else
-
raise AddressFamilyError, "unsupported address family"
-
end
-
end
-
-
# Returns true if the ipaddr is an IPv4 address.
-
1
def ipv4?
-
return @family == Socket::AF_INET
-
end
-
-
# Returns true if the ipaddr is an IPv6 address.
-
1
def ipv6?
-
return @family == Socket::AF_INET6
-
end
-
-
# Returns true if the ipaddr is a loopback address.
-
1
def loopback?
-
case @family
-
when Socket::AF_INET
-
@addr & 0xff000000 == 0x7f000000
-
when Socket::AF_INET6
-
@addr == 1
-
else
-
raise AddressFamilyError, "unsupported address family"
-
end
-
end
-
-
# Returns true if the ipaddr is a private address. IPv4 addresses
-
# in 10.0.0.0/8, 172.16.0.0/12 and 192.168.0.0/16 as defined in RFC
-
# 1918 and IPv6 Unique Local Addresses in fc00::/7 as defined in RFC
-
# 4193 are considered private.
-
1
def private?
-
case @family
-
when Socket::AF_INET
-
@addr & 0xff000000 == 0x0a000000 || # 10.0.0.0/8
-
@addr & 0xfff00000 == 0xac100000 || # 172.16.0.0/12
-
@addr & 0xffff0000 == 0xc0a80000 # 192.168.0.0/16
-
when Socket::AF_INET6
-
@addr & 0xfe00_0000_0000_0000_0000_0000_0000_0000 == 0xfc00_0000_0000_0000_0000_0000_0000_0000
-
else
-
raise AddressFamilyError, "unsupported address family"
-
end
-
end
-
-
# Returns true if the ipaddr is a link-local address. IPv4
-
# addresses in 169.254.0.0/16 reserved by RFC 3927 and Link-Local
-
# IPv6 Unicast Addresses in fe80::/10 reserved by RFC 4291 are
-
# considered link-local.
-
1
def link_local?
-
case @family
-
when Socket::AF_INET
-
@addr & 0xffff0000 == 0xa9fe0000 # 169.254.0.0/16
-
when Socket::AF_INET6
-
@addr & 0xffc0_0000_0000_0000_0000_0000_0000_0000 == 0xfe80_0000_0000_0000_0000_0000_0000_0000
-
else
-
raise AddressFamilyError, "unsupported address family"
-
end
-
end
-
-
# Returns true if the ipaddr is an IPv4-mapped IPv6 address.
-
1
def ipv4_mapped?
-
return ipv6? && (@addr >> 32) == 0xffff
-
end
-
-
# Returns true if the ipaddr is an IPv4-compatible IPv6 address.
-
1
def ipv4_compat?
-
warn "IPAddr\##{__callee__} is obsolete", uplevel: 1 if $VERBOSE
-
_ipv4_compat?
-
end
-
-
1
def _ipv4_compat?
-
if !ipv6? || (@addr >> 32) != 0
-
return false
-
end
-
a = (@addr & IN4MASK)
-
return a != 0 && a != 1
-
end
-
-
1
private :_ipv4_compat?
-
-
# Returns a new ipaddr built by converting the native IPv4 address
-
# into an IPv4-mapped IPv6 address.
-
1
def ipv4_mapped
-
if !ipv4?
-
raise InvalidAddressError, "not an IPv4 address"
-
end
-
return self.clone.set(@addr | 0xffff00000000, Socket::AF_INET6)
-
end
-
-
# Returns a new ipaddr built by converting the native IPv4 address
-
# into an IPv4-compatible IPv6 address.
-
1
def ipv4_compat
-
warn "IPAddr\##{__callee__} is obsolete", uplevel: 1 if $VERBOSE
-
if !ipv4?
-
raise InvalidAddressError, "not an IPv4 address"
-
end
-
return self.clone.set(@addr, Socket::AF_INET6)
-
end
-
-
# Returns a new ipaddr built by converting the IPv6 address into a
-
# native IPv4 address. If the IP address is not an IPv4-mapped or
-
# IPv4-compatible IPv6 address, returns self.
-
1
def native
-
if !ipv4_mapped? && !_ipv4_compat?
-
return self
-
end
-
return self.clone.set(@addr & IN4MASK, Socket::AF_INET)
-
end
-
-
# Returns a string for DNS reverse lookup. It returns a string in
-
# RFC3172 form for an IPv6 address.
-
1
def reverse
-
case @family
-
when Socket::AF_INET
-
return _reverse + ".in-addr.arpa"
-
when Socket::AF_INET6
-
return ip6_arpa
-
else
-
raise AddressFamilyError, "unsupported address family"
-
end
-
end
-
-
# Returns a string for DNS reverse lookup compatible with RFC3172.
-
1
def ip6_arpa
-
if !ipv6?
-
raise InvalidAddressError, "not an IPv6 address"
-
end
-
return _reverse + ".ip6.arpa"
-
end
-
-
# Returns a string for DNS reverse lookup compatible with RFC1886.
-
1
def ip6_int
-
if !ipv6?
-
raise InvalidAddressError, "not an IPv6 address"
-
end
-
return _reverse + ".ip6.int"
-
end
-
-
# Returns the successor to the ipaddr.
-
1
def succ
-
return self.clone.set(@addr + 1, @family)
-
end
-
-
# Compares the ipaddr with another.
-
1
def <=>(other)
-
other = coerce_other(other)
-
rescue
-
nil
-
else
-
@addr <=> other.to_i if other.family == @family
-
end
-
1
include Comparable
-
-
# Checks equality used by Hash.
-
1
def eql?(other)
-
return self.class == other.class && self.hash == other.hash && self == other
-
end
-
-
# Returns a hash value used by Hash, Set, and Array classes
-
1
def hash
-
return ([@addr, @mask_addr].hash << 1) | (ipv4? ? 0 : 1)
-
end
-
-
# Creates a Range object for the network address.
-
1
def to_range
-
begin_addr = (@addr & @mask_addr)
-
-
case @family
-
when Socket::AF_INET
-
end_addr = (@addr | (IN4MASK ^ @mask_addr))
-
when Socket::AF_INET6
-
end_addr = (@addr | (IN6MASK ^ @mask_addr))
-
else
-
raise AddressFamilyError, "unsupported address family"
-
end
-
-
return clone.set(begin_addr, @family)..clone.set(end_addr, @family)
-
end
-
-
# Returns the prefix length in bits for the ipaddr.
-
1
def prefix
-
case @family
-
when Socket::AF_INET
-
n = IN4MASK ^ @mask_addr
-
i = 32
-
when Socket::AF_INET6
-
n = IN6MASK ^ @mask_addr
-
i = 128
-
else
-
raise AddressFamilyError, "unsupported address family"
-
end
-
while n.positive?
-
n >>= 1
-
i -= 1
-
end
-
i
-
end
-
-
# Sets the prefix length in bits
-
1
def prefix=(prefix)
-
case prefix
-
when Integer
-
mask!(prefix)
-
else
-
raise InvalidPrefixError, "prefix must be an integer"
-
end
-
end
-
-
# Returns a string containing a human-readable representation of the
-
# ipaddr. ("#<IPAddr: family:address/mask>")
-
1
def inspect
-
case @family
-
when Socket::AF_INET
-
af = "IPv4"
-
when Socket::AF_INET6
-
af = "IPv6"
-
else
-
raise AddressFamilyError, "unsupported address family"
-
end
-
return sprintf("#<%s: %s:%s/%s>", self.class.name,
-
af, _to_string(@addr), _to_string(@mask_addr))
-
end
-
-
1
protected
-
-
# Set +@addr+, the internal stored ip address, to given +addr+. The
-
# parameter +addr+ is validated using the first +family+ member,
-
# which is +Socket::AF_INET+ or +Socket::AF_INET6+.
-
1
def set(addr, *family)
-
case family[0] ? family[0] : @family
-
when Socket::AF_INET
-
if addr < 0 || addr > IN4MASK
-
raise InvalidAddressError, "invalid address"
-
end
-
when Socket::AF_INET6
-
if addr < 0 || addr > IN6MASK
-
raise InvalidAddressError, "invalid address"
-
end
-
else
-
raise AddressFamilyError, "unsupported address family"
-
end
-
@addr = addr
-
if family[0]
-
@family = family[0]
-
end
-
return self
-
end
-
-
# Set current netmask to given mask.
-
1
def mask!(mask)
-
case mask
-
when String
-
if mask =~ /\A\d+\z/
-
prefixlen = mask.to_i
-
else
-
m = IPAddr.new(mask)
-
if m.family != @family
-
raise InvalidPrefixError, "address family is not same"
-
end
-
@mask_addr = m.to_i
-
n = @mask_addr ^ m.instance_variable_get(:@mask_addr)
-
unless ((n + 1) & n).zero?
-
raise InvalidPrefixError, "invalid mask #{mask}"
-
end
-
@addr &= @mask_addr
-
return self
-
end
-
else
-
prefixlen = mask
-
end
-
case @family
-
when Socket::AF_INET
-
if prefixlen < 0 || prefixlen > 32
-
raise InvalidPrefixError, "invalid length"
-
end
-
masklen = 32 - prefixlen
-
@mask_addr = ((IN4MASK >> masklen) << masklen)
-
when Socket::AF_INET6
-
if prefixlen < 0 || prefixlen > 128
-
raise InvalidPrefixError, "invalid length"
-
end
-
masklen = 128 - prefixlen
-
@mask_addr = ((IN6MASK >> masklen) << masklen)
-
else
-
raise AddressFamilyError, "unsupported address family"
-
end
-
@addr = ((@addr >> masklen) << masklen)
-
return self
-
end
-
-
1
private
-
-
# Creates a new ipaddr object either from a human readable IP
-
# address representation in string, or from a packed in_addr value
-
# followed by an address family.
-
#
-
# In the former case, the following are the valid formats that will
-
# be recognized: "address", "address/prefixlen" and "address/mask",
-
# where IPv6 address may be enclosed in square brackets (`[' and
-
# `]'). If a prefixlen or a mask is specified, it returns a masked
-
# IP address. Although the address family is determined
-
# automatically from a specified string, you can specify one
-
# explicitly by the optional second argument.
-
#
-
# Otherwise an IP address is generated from a packed in_addr value
-
# and an address family.
-
#
-
# The IPAddr class defines many methods and operators, and some of
-
# those, such as &, |, include? and ==, accept a string, or a packed
-
# in_addr value instead of an IPAddr object.
-
1
def initialize(addr = '::', family = Socket::AF_UNSPEC)
-
if !addr.kind_of?(String)
-
case family
-
when Socket::AF_INET, Socket::AF_INET6
-
set(addr.to_i, family)
-
@mask_addr = (family == Socket::AF_INET) ? IN4MASK : IN6MASK
-
return
-
when Socket::AF_UNSPEC
-
raise AddressFamilyError, "address family must be specified"
-
else
-
raise AddressFamilyError, "unsupported address family: #{family}"
-
end
-
end
-
prefix, prefixlen = addr.split('/')
-
if prefix =~ /\A\[(.*)\]\z/i
-
prefix = $1
-
family = Socket::AF_INET6
-
end
-
# It seems AI_NUMERICHOST doesn't do the job.
-
#Socket.getaddrinfo(left, nil, Socket::AF_INET6, Socket::SOCK_STREAM, nil,
-
# Socket::AI_NUMERICHOST)
-
@addr = @family = nil
-
if family == Socket::AF_UNSPEC || family == Socket::AF_INET
-
@addr = in_addr(prefix)
-
if @addr
-
@family = Socket::AF_INET
-
end
-
end
-
if !@addr && (family == Socket::AF_UNSPEC || family == Socket::AF_INET6)
-
@addr = in6_addr(prefix)
-
@family = Socket::AF_INET6
-
end
-
if family != Socket::AF_UNSPEC && @family != family
-
raise AddressFamilyError, "address family mismatch"
-
end
-
if prefixlen
-
mask!(prefixlen)
-
else
-
@mask_addr = (@family == Socket::AF_INET) ? IN4MASK : IN6MASK
-
end
-
rescue InvalidAddressError => e
-
raise e.class, "#{e.message}: #{addr}"
-
end
-
-
1
def coerce_other(other)
-
case other
-
when IPAddr
-
other
-
when String
-
self.class.new(other)
-
else
-
self.class.new(other, @family)
-
end
-
end
-
-
1
def in_addr(addr)
-
case addr
-
when Array
-
octets = addr
-
else
-
m = RE_IPV4ADDRLIKE.match(addr) or return nil
-
octets = m.captures
-
end
-
octets.inject(0) { |i, s|
-
(n = s.to_i) < 256 or raise InvalidAddressError, "invalid address"
-
s.match(/\A0./) and raise InvalidAddressError, "zero-filled number in IPv4 address is ambiguous"
-
i << 8 | n
-
}
-
end
-
-
1
def in6_addr(left)
-
case left
-
when RE_IPV6ADDRLIKE_FULL
-
if $2
-
addr = in_addr($~[2,4])
-
left = $1 + ':'
-
else
-
addr = 0
-
end
-
right = ''
-
when RE_IPV6ADDRLIKE_COMPRESSED
-
if $4
-
left.count(':') <= 6 or raise InvalidAddressError, "invalid address"
-
addr = in_addr($~[4,4])
-
left = $1
-
right = $3 + '0:0'
-
else
-
left.count(':') <= ($1.empty? || $2.empty? ? 8 : 7) or
-
raise InvalidAddressError, "invalid address"
-
left = $1
-
right = $2
-
addr = 0
-
end
-
else
-
raise InvalidAddressError, "invalid address"
-
end
-
l = left.split(':')
-
r = right.split(':')
-
rest = 8 - l.size - r.size
-
if rest < 0
-
return nil
-
end
-
(l + Array.new(rest, '0') + r).inject(0) { |i, s|
-
i << 16 | s.hex
-
} | addr
-
end
-
-
1
def addr_mask(addr)
-
case @family
-
when Socket::AF_INET
-
return addr & IN4MASK
-
when Socket::AF_INET6
-
return addr & IN6MASK
-
else
-
raise AddressFamilyError, "unsupported address family"
-
end
-
end
-
-
1
def _reverse
-
case @family
-
when Socket::AF_INET
-
return (0..3).map { |i|
-
(@addr >> (8 * i)) & 0xff
-
}.join('.')
-
when Socket::AF_INET6
-
return ("%.32x" % @addr).reverse!.gsub!(/.(?!$)/, '\&.')
-
else
-
raise AddressFamilyError, "unsupported address family"
-
end
-
end
-
-
1
def _to_string(addr)
-
case @family
-
when Socket::AF_INET
-
return (0..3).map { |i|
-
(addr >> (24 - 8 * i)) & 0xff
-
}.join('.')
-
when Socket::AF_INET6
-
return (("%.32x" % addr).gsub!(/.{4}(?!$)/, '\&:'))
-
else
-
raise AddressFamilyError, "unsupported address family"
-
end
-
end
-
-
end
-
-
1
unless Socket.const_defined? :AF_INET6
-
class Socket < BasicSocket
-
# IPv6 protocol family
-
AF_INET6 = Object.new
-
end
-
-
class << IPSocket
-
private
-
-
def valid_v6?(addr)
-
case addr
-
when IPAddr::RE_IPV6ADDRLIKE_FULL
-
if $2
-
$~[2,4].all? {|i| i.to_i < 256 }
-
else
-
true
-
end
-
when IPAddr::RE_IPV6ADDRLIKE_COMPRESSED
-
if $4
-
addr.count(':') <= 6 && $~[4,4].all? {|i| i.to_i < 256}
-
else
-
addr.count(':') <= 7
-
end
-
else
-
false
-
end
-
end
-
-
alias getaddress_orig getaddress
-
-
public
-
-
# Returns a +String+ based representation of a valid DNS hostname,
-
# IPv4 or IPv6 address.
-
#
-
# IPSocket.getaddress 'localhost' #=> "::1"
-
# IPSocket.getaddress 'broadcasthost' #=> "255.255.255.255"
-
# IPSocket.getaddress 'www.ruby-lang.org' #=> "221.186.184.68"
-
# IPSocket.getaddress 'www.ccc.de' #=> "2a00:1328:e102:ccc0::122"
-
def getaddress(s)
-
if valid_v6?(s)
-
s
-
else
-
getaddress_orig(s)
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
# logger.rb - simple logging utility
-
# Copyright (C) 2000-2003, 2005, 2008, 2011 NAKAMURA, Hiroshi <nahi@ruby-lang.org>.
-
#
-
# Documentation:: NAKAMURA, Hiroshi and Gavin Sinclair
-
# License::
-
# You can redistribute it and/or modify it under the same terms of Ruby's
-
# license; either the dual license version in 2003, or any later version.
-
# Revision:: $Id$
-
#
-
# A simple system for logging messages. See Logger for more documentation.
-
-
1
require 'monitor'
-
-
1
require_relative 'logger/version'
-
1
require_relative 'logger/formatter'
-
1
require_relative 'logger/log_device'
-
1
require_relative 'logger/severity'
-
1
require_relative 'logger/errors'
-
-
# == Description
-
#
-
# The Logger class provides a simple but sophisticated logging utility that
-
# you can use to output messages.
-
#
-
# The messages have associated levels, such as +INFO+ or +ERROR+ that indicate
-
# their importance. You can then give the Logger a level, and only messages
-
# at that level or higher will be printed.
-
#
-
# The levels are:
-
#
-
# +UNKNOWN+:: An unknown message that should always be logged.
-
# +FATAL+:: An unhandleable error that results in a program crash.
-
# +ERROR+:: A handleable error condition.
-
# +WARN+:: A warning.
-
# +INFO+:: Generic (useful) information about system operation.
-
# +DEBUG+:: Low-level information for developers.
-
#
-
# For instance, in a production system, you may have your Logger set to
-
# +INFO+ or even +WARN+.
-
# When you are developing the system, however, you probably
-
# want to know about the program's internal state, and would set the Logger to
-
# +DEBUG+.
-
#
-
# *Note*: Logger does not escape or sanitize any messages passed to it.
-
# Developers should be aware of when potentially malicious data (user-input)
-
# is passed to Logger, and manually escape the untrusted data:
-
#
-
# logger.info("User-input: #{input.dump}")
-
# logger.info("User-input: %p" % input)
-
#
-
# You can use #formatter= for escaping all data.
-
#
-
# original_formatter = Logger::Formatter.new
-
# logger.formatter = proc { |severity, datetime, progname, msg|
-
# original_formatter.call(severity, datetime, progname, msg.dump)
-
# }
-
# logger.info(input)
-
#
-
# === Example
-
#
-
# This creates a Logger that outputs to the standard output stream, with a
-
# level of +WARN+:
-
#
-
# require 'logger'
-
#
-
# logger = Logger.new(STDOUT)
-
# logger.level = Logger::WARN
-
#
-
# logger.debug("Created logger")
-
# logger.info("Program started")
-
# logger.warn("Nothing to do!")
-
#
-
# path = "a_non_existent_file"
-
#
-
# begin
-
# File.foreach(path) do |line|
-
# unless line =~ /^(\w+) = (.*)$/
-
# logger.error("Line in wrong format: #{line.chomp}")
-
# end
-
# end
-
# rescue => err
-
# logger.fatal("Caught exception; exiting")
-
# logger.fatal(err)
-
# end
-
#
-
# Because the Logger's level is set to +WARN+, only the warning, error, and
-
# fatal messages are recorded. The debug and info messages are silently
-
# discarded.
-
#
-
# === Features
-
#
-
# There are several interesting features that Logger provides, like
-
# auto-rolling of log files, setting the format of log messages, and
-
# specifying a program name in conjunction with the message. The next section
-
# shows you how to achieve these things.
-
#
-
#
-
# == HOWTOs
-
#
-
# === How to create a logger
-
#
-
# The options below give you various choices, in more or less increasing
-
# complexity.
-
#
-
# 1. Create a logger which logs messages to STDERR/STDOUT.
-
#
-
# logger = Logger.new(STDERR)
-
# logger = Logger.new(STDOUT)
-
#
-
# 2. Create a logger for the file which has the specified name.
-
#
-
# logger = Logger.new('logfile.log')
-
#
-
# 3. Create a logger for the specified file.
-
#
-
# file = File.open('foo.log', File::WRONLY | File::APPEND)
-
# # To create new logfile, add File::CREAT like:
-
# # file = File.open('foo.log', File::WRONLY | File::APPEND | File::CREAT)
-
# logger = Logger.new(file)
-
#
-
# 4. Create a logger which ages the logfile once it reaches a certain size.
-
# Leave 10 "old" log files where each file is about 1,024,000 bytes.
-
#
-
# logger = Logger.new('foo.log', 10, 1024000)
-
#
-
# 5. Create a logger which ages the logfile daily/weekly/monthly.
-
#
-
# logger = Logger.new('foo.log', 'daily')
-
# logger = Logger.new('foo.log', 'weekly')
-
# logger = Logger.new('foo.log', 'monthly')
-
#
-
# === How to log a message
-
#
-
# Notice the different methods (+fatal+, +error+, +info+) being used to log
-
# messages of various levels? Other methods in this family are +warn+ and
-
# +debug+. +add+ is used below to log a message of an arbitrary (perhaps
-
# dynamic) level.
-
#
-
# 1. Message in a block.
-
#
-
# logger.fatal { "Argument 'foo' not given." }
-
#
-
# 2. Message as a string.
-
#
-
# logger.error "Argument #{@foo} mismatch."
-
#
-
# 3. With progname.
-
#
-
# logger.info('initialize') { "Initializing..." }
-
#
-
# 4. With severity.
-
#
-
# logger.add(Logger::FATAL) { 'Fatal error!' }
-
#
-
# The block form allows you to create potentially complex log messages,
-
# but to delay their evaluation until and unless the message is
-
# logged. For example, if we have the following:
-
#
-
# logger.debug { "This is a " + potentially + " expensive operation" }
-
#
-
# If the logger's level is +INFO+ or higher, no debug messages will be logged,
-
# and the entire block will not even be evaluated. Compare to this:
-
#
-
# logger.debug("This is a " + potentially + " expensive operation")
-
#
-
# Here, the string concatenation is done every time, even if the log
-
# level is not set to show the debug message.
-
#
-
# === How to close a logger
-
#
-
# logger.close
-
#
-
# === Setting severity threshold
-
#
-
# 1. Original interface.
-
#
-
# logger.sev_threshold = Logger::WARN
-
#
-
# 2. Log4r (somewhat) compatible interface.
-
#
-
# logger.level = Logger::INFO
-
#
-
# # DEBUG < INFO < WARN < ERROR < FATAL < UNKNOWN
-
#
-
# 3. Symbol or String (case insensitive)
-
#
-
# logger.level = :info
-
# logger.level = 'INFO'
-
#
-
# # :debug < :info < :warn < :error < :fatal < :unknown
-
#
-
# 4. Constructor
-
#
-
# Logger.new(logdev, level: Logger::INFO)
-
# Logger.new(logdev, level: :info)
-
# Logger.new(logdev, level: 'INFO')
-
#
-
# == Format
-
#
-
# Log messages are rendered in the output stream in a certain format by
-
# default. The default format and a sample are shown below:
-
#
-
# Log format:
-
# SeverityID, [DateTime #pid] SeverityLabel -- ProgName: message
-
#
-
# Log sample:
-
# I, [1999-03-03T02:34:24.895701 #19074] INFO -- Main: info.
-
#
-
# You may change the date and time format via #datetime_format=.
-
#
-
# logger.datetime_format = '%Y-%m-%d %H:%M:%S'
-
# # e.g. "2004-01-03 00:54:26"
-
#
-
# or via the constructor.
-
#
-
# Logger.new(logdev, datetime_format: '%Y-%m-%d %H:%M:%S')
-
#
-
# Or, you may change the overall format via the #formatter= method.
-
#
-
# logger.formatter = proc do |severity, datetime, progname, msg|
-
# "#{datetime}: #{msg}\n"
-
# end
-
# # e.g. "2005-09-22 08:51:08 +0900: hello world"
-
#
-
# or via the constructor.
-
#
-
# Logger.new(logdev, formatter: proc {|severity, datetime, progname, msg|
-
# "#{datetime}: #{msg}\n"
-
# })
-
#
-
1
class Logger
-
1
_, name, rev = %w$Id$
-
1
if name
-
name = name.chomp(",v")
-
else
-
1
name = File.basename(__FILE__)
-
end
-
1
rev ||= "v#{VERSION}"
-
1
ProgName = "#{name}/#{rev}"
-
-
1
include Severity
-
-
# Logging severity threshold (e.g. <tt>Logger::INFO</tt>).
-
1
attr_reader :level
-
-
# Set logging severity threshold.
-
#
-
# +severity+:: The Severity of the log message.
-
1
def level=(severity)
-
if severity.is_a?(Integer)
-
@level = severity
-
else
-
case severity.to_s.downcase
-
when 'debug'
-
@level = DEBUG
-
when 'info'
-
@level = INFO
-
when 'warn'
-
@level = WARN
-
when 'error'
-
@level = ERROR
-
when 'fatal'
-
@level = FATAL
-
when 'unknown'
-
@level = UNKNOWN
-
else
-
raise ArgumentError, "invalid log level: #{severity}"
-
end
-
end
-
end
-
-
# Program name to include in log messages.
-
1
attr_accessor :progname
-
-
# Set date-time format.
-
#
-
# +datetime_format+:: A string suitable for passing to +strftime+.
-
1
def datetime_format=(datetime_format)
-
@default_formatter.datetime_format = datetime_format
-
end
-
-
# Returns the date format being used. See #datetime_format=
-
1
def datetime_format
-
@default_formatter.datetime_format
-
end
-
-
# Logging formatter, as a +Proc+ that will take four arguments and
-
# return the formatted message. The arguments are:
-
#
-
# +severity+:: The Severity of the log message.
-
# +time+:: A Time instance representing when the message was logged.
-
# +progname+:: The #progname configured, or passed to the logger method.
-
# +msg+:: The _Object_ the user passed to the log message; not necessarily a
-
# String.
-
#
-
# The block should return an Object that can be written to the logging
-
# device via +write+. The default formatter is used when no formatter is
-
# set.
-
1
attr_accessor :formatter
-
-
1
alias sev_threshold level
-
1
alias sev_threshold= level=
-
-
# Returns +true+ iff the current severity level allows for the printing of
-
# +DEBUG+ messages.
-
1
def debug?; level <= DEBUG; end
-
-
# Sets the severity to DEBUG.
-
1
def debug!; self.level = DEBUG; end
-
-
# Returns +true+ iff the current severity level allows for the printing of
-
# +INFO+ messages.
-
1
def info?; level <= INFO; end
-
-
# Sets the severity to INFO.
-
1
def info!; self.level = INFO; end
-
-
# Returns +true+ iff the current severity level allows for the printing of
-
# +WARN+ messages.
-
1
def warn?; level <= WARN; end
-
-
# Sets the severity to WARN.
-
1
def warn!; self.level = WARN; end
-
-
# Returns +true+ iff the current severity level allows for the printing of
-
# +ERROR+ messages.
-
1
def error?; level <= ERROR; end
-
-
# Sets the severity to ERROR.
-
1
def error!; self.level = ERROR; end
-
-
# Returns +true+ iff the current severity level allows for the printing of
-
# +FATAL+ messages.
-
1
def fatal?; level <= FATAL; end
-
-
# Sets the severity to FATAL.
-
1
def fatal!; self.level = FATAL; end
-
-
#
-
# :call-seq:
-
# Logger.new(logdev, shift_age = 0, shift_size = 1048576)
-
# Logger.new(logdev, shift_age = 'weekly')
-
# Logger.new(logdev, level: :info)
-
# Logger.new(logdev, progname: 'progname')
-
# Logger.new(logdev, formatter: formatter)
-
# Logger.new(logdev, datetime_format: '%Y-%m-%d %H:%M:%S')
-
#
-
# === Args
-
#
-
# +logdev+::
-
# The log device. This is a filename (String) or IO object (typically
-
# +STDOUT+, +STDERR+, or an open file).
-
# +shift_age+::
-
# Number of old log files to keep, *or* frequency of rotation (+daily+,
-
# +weekly+ or +monthly+). Default value is 0, which disables log file
-
# rotation.
-
# +shift_size+::
-
# Maximum logfile size in bytes (only applies when +shift_age+ is a positive
-
# Integer). Defaults to +1048576+ (1MB).
-
# +level+::
-
# Logging severity threshold. Default values is Logger::DEBUG.
-
# +progname+::
-
# Program name to include in log messages. Default value is nil.
-
# +formatter+::
-
# Logging formatter. Default values is an instance of Logger::Formatter.
-
# +datetime_format+::
-
# Date and time format. Default value is '%Y-%m-%d %H:%M:%S'.
-
# +binmode+::
-
# Use binary mode on the log device. Default value is false.
-
# +shift_period_suffix+::
-
# The log file suffix format for +daily+, +weekly+ or +monthly+ rotation.
-
# Default is '%Y%m%d'.
-
#
-
# === Description
-
#
-
# Create an instance.
-
#
-
1
def initialize(logdev, shift_age = 0, shift_size = 1048576, level: DEBUG,
-
progname: nil, formatter: nil, datetime_format: nil,
-
binmode: false, shift_period_suffix: '%Y%m%d')
-
self.level = level
-
self.progname = progname
-
@default_formatter = Formatter.new
-
self.datetime_format = datetime_format
-
self.formatter = formatter
-
@logdev = nil
-
if logdev
-
@logdev = LogDevice.new(logdev, shift_age: shift_age,
-
shift_size: shift_size,
-
shift_period_suffix: shift_period_suffix,
-
binmode: binmode)
-
end
-
end
-
-
#
-
# :call-seq:
-
# Logger#reopen
-
# Logger#reopen(logdev)
-
#
-
# === Args
-
#
-
# +logdev+::
-
# The log device. This is a filename (String) or IO object (typically
-
# +STDOUT+, +STDERR+, or an open file). reopen the same filename if
-
# it is +nil+, do nothing for IO. Default is +nil+.
-
#
-
# === Description
-
#
-
# Reopen a log device.
-
#
-
1
def reopen(logdev = nil)
-
@logdev.reopen(logdev)
-
self
-
end
-
-
#
-
# :call-seq:
-
# Logger#add(severity, message = nil, progname = nil) { ... }
-
#
-
# === Args
-
#
-
# +severity+::
-
# Severity. Constants are defined in Logger namespace: +DEBUG+, +INFO+,
-
# +WARN+, +ERROR+, +FATAL+, or +UNKNOWN+.
-
# +message+::
-
# The log message. A String or Exception.
-
# +progname+::
-
# Program name string. Can be omitted. Treated as a message if no
-
# +message+ and +block+ are given.
-
# +block+::
-
# Can be omitted. Called to get a message string if +message+ is nil.
-
#
-
# === Return
-
#
-
# When the given severity is not high enough (for this particular logger),
-
# log no message, and return +true+.
-
#
-
# === Description
-
#
-
# Log a message if the given severity is high enough. This is the generic
-
# logging method. Users will be more inclined to use #debug, #info, #warn,
-
# #error, and #fatal.
-
#
-
# <b>Message format</b>: +message+ can be any object, but it has to be
-
# converted to a String in order to log it. Generally, +inspect+ is used
-
# if the given object is not a String.
-
# A special case is an +Exception+ object, which will be printed in detail,
-
# including message, class, and backtrace. See #msg2str for the
-
# implementation if required.
-
#
-
# === Bugs
-
#
-
# * Logfile is not locked.
-
# * Append open does not need to lock file.
-
# * If the OS supports multi I/O, records possibly may be mixed.
-
#
-
1
def add(severity, message = nil, progname = nil)
-
severity ||= UNKNOWN
-
if @logdev.nil? or severity < level
-
return true
-
end
-
if progname.nil?
-
progname = @progname
-
end
-
if message.nil?
-
if block_given?
-
message = yield
-
else
-
message = progname
-
progname = @progname
-
end
-
end
-
@logdev.write(
-
format_message(format_severity(severity), Time.now, progname, message))
-
true
-
end
-
1
alias log add
-
-
#
-
# Dump given message to the log device without any formatting. If no log
-
# device exists, return +nil+.
-
#
-
1
def <<(msg)
-
@logdev&.write(msg)
-
end
-
-
#
-
# Log a +DEBUG+ message.
-
#
-
# See #info for more information.
-
#
-
1
def debug(progname = nil, &block)
-
add(DEBUG, nil, progname, &block)
-
end
-
-
#
-
# :call-seq:
-
# info(message)
-
# info(progname, &block)
-
#
-
# Log an +INFO+ message.
-
#
-
# +message+:: The message to log; does not need to be a String.
-
# +progname+:: In the block form, this is the #progname to use in the
-
# log message. The default can be set with #progname=.
-
# +block+:: Evaluates to the message to log. This is not evaluated unless
-
# the logger's level is sufficient to log the message. This
-
# allows you to create potentially expensive logging messages that
-
# are only called when the logger is configured to show them.
-
#
-
# === Examples
-
#
-
# logger.info("MainApp") { "Received connection from #{ip}" }
-
# # ...
-
# logger.info "Waiting for input from user"
-
# # ...
-
# logger.info { "User typed #{input}" }
-
#
-
# You'll probably stick to the second form above, unless you want to provide a
-
# program name (which you can do with #progname= as well).
-
#
-
# === Return
-
#
-
# See #add.
-
#
-
1
def info(progname = nil, &block)
-
add(INFO, nil, progname, &block)
-
end
-
-
#
-
# Log a +WARN+ message.
-
#
-
# See #info for more information.
-
#
-
1
def warn(progname = nil, &block)
-
add(WARN, nil, progname, &block)
-
end
-
-
#
-
# Log an +ERROR+ message.
-
#
-
# See #info for more information.
-
#
-
1
def error(progname = nil, &block)
-
add(ERROR, nil, progname, &block)
-
end
-
-
#
-
# Log a +FATAL+ message.
-
#
-
# See #info for more information.
-
#
-
1
def fatal(progname = nil, &block)
-
add(FATAL, nil, progname, &block)
-
end
-
-
#
-
# Log an +UNKNOWN+ message. This will be printed no matter what the logger's
-
# level is.
-
#
-
# See #info for more information.
-
#
-
1
def unknown(progname = nil, &block)
-
add(UNKNOWN, nil, progname, &block)
-
end
-
-
#
-
# Close the logging device.
-
#
-
1
def close
-
@logdev&.close
-
end
-
-
1
private
-
-
# Severity label for logging (max 5 chars).
-
1
SEV_LABEL = %w(DEBUG INFO WARN ERROR FATAL ANY).freeze
-
-
1
def format_severity(severity)
-
SEV_LABEL[severity] || 'ANY'
-
end
-
-
1
def format_message(severity, datetime, progname, msg)
-
(@formatter || @default_formatter).call(severity, datetime, progname, msg)
-
end
-
end
-
# frozen_string_literal: true
-
-
# not used after 1.2.7. just for compat.
-
1
class Logger
-
1
class Error < RuntimeError # :nodoc:
-
end
-
1
class ShiftingError < Error # :nodoc:
-
end
-
end
-
# frozen_string_literal: true
-
-
1
class Logger
-
# Default formatter for log messages.
-
1
class Formatter
-
1
Format = "%s, [%s#%d] %5s -- %s: %s\n"
-
-
1
attr_accessor :datetime_format
-
-
1
def initialize
-
@datetime_format = nil
-
end
-
-
1
def call(severity, time, progname, msg)
-
Format % [severity[0..0], format_datetime(time), $$, severity, progname,
-
msg2str(msg)]
-
end
-
-
1
private
-
-
1
def format_datetime(time)
-
time.strftime(@datetime_format || "%Y-%m-%dT%H:%M:%S.%6N ")
-
end
-
-
1
def msg2str(msg)
-
case msg
-
when ::String
-
msg
-
when ::Exception
-
"#{ msg.message } (#{ msg.class })\n#{ msg.backtrace.join("\n") if msg.backtrace }"
-
else
-
msg.inspect
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require_relative 'period'
-
-
1
class Logger
-
# Device used for logging messages.
-
1
class LogDevice
-
1
include Period
-
-
1
attr_reader :dev
-
1
attr_reader :filename
-
1
include MonitorMixin
-
-
1
def initialize(log = nil, shift_age: nil, shift_size: nil, shift_period_suffix: nil, binmode: false)
-
@dev = @filename = @shift_age = @shift_size = @shift_period_suffix = nil
-
@binmode = binmode
-
mon_initialize
-
set_dev(log)
-
if @filename
-
@shift_age = shift_age || 7
-
@shift_size = shift_size || 1048576
-
@shift_period_suffix = shift_period_suffix || '%Y%m%d'
-
-
unless @shift_age.is_a?(Integer)
-
base_time = @dev.respond_to?(:stat) ? @dev.stat.mtime : Time.now
-
@next_rotate_time = next_rotate_time(base_time, @shift_age)
-
end
-
end
-
end
-
-
1
def write(message)
-
begin
-
synchronize do
-
if @shift_age and @dev.respond_to?(:stat)
-
begin
-
check_shift_log
-
rescue
-
warn("log shifting failed. #{$!}")
-
end
-
end
-
begin
-
@dev.write(message)
-
rescue
-
warn("log writing failed. #{$!}")
-
end
-
end
-
rescue Exception => ignored
-
warn("log writing failed. #{ignored}")
-
end
-
end
-
-
1
def close
-
begin
-
synchronize do
-
@dev.close rescue nil
-
end
-
rescue Exception
-
@dev.close rescue nil
-
end
-
end
-
-
1
def reopen(log = nil)
-
# reopen the same filename if no argument, do nothing for IO
-
log ||= @filename if @filename
-
if log
-
synchronize do
-
if @filename and @dev
-
@dev.close rescue nil # close only file opened by Logger
-
@filename = nil
-
end
-
set_dev(log)
-
end
-
end
-
self
-
end
-
-
1
private
-
-
1
def set_dev(log)
-
if log.respond_to?(:write) and log.respond_to?(:close)
-
@dev = log
-
if log.respond_to?(:path)
-
@filename = log.path
-
end
-
else
-
@dev = open_logfile(log)
-
@dev.sync = true
-
@dev.binmode if @binmode
-
@filename = log
-
end
-
end
-
-
1
def open_logfile(filename)
-
begin
-
File.open(filename, (File::WRONLY | File::APPEND))
-
rescue Errno::ENOENT
-
create_logfile(filename)
-
end
-
end
-
-
1
def create_logfile(filename)
-
begin
-
logdev = File.open(filename, (File::WRONLY | File::APPEND | File::CREAT | File::EXCL))
-
logdev.flock(File::LOCK_EX)
-
logdev.sync = true
-
logdev.binmode if @binmode
-
add_log_header(logdev)
-
logdev.flock(File::LOCK_UN)
-
rescue Errno::EEXIST
-
# file is created by another process
-
logdev = open_logfile(filename)
-
logdev.sync = true
-
end
-
logdev
-
end
-
-
1
def add_log_header(file)
-
file.write(
-
"# Logfile created on %s by %s\n" % [Time.now.to_s, Logger::ProgName]
-
) if file.size == 0
-
end
-
-
1
def check_shift_log
-
if @shift_age.is_a?(Integer)
-
# Note: always returns false if '0'.
-
if @filename && (@shift_age > 0) && (@dev.stat.size > @shift_size)
-
lock_shift_log { shift_log_age }
-
end
-
else
-
now = Time.now
-
if now >= @next_rotate_time
-
@next_rotate_time = next_rotate_time(now, @shift_age)
-
lock_shift_log { shift_log_period(previous_period_end(now, @shift_age)) }
-
end
-
end
-
end
-
-
1
if /mswin|mingw/ =~ RUBY_PLATFORM
-
def lock_shift_log
-
yield
-
end
-
else
-
1
def lock_shift_log
-
retry_limit = 8
-
retry_sleep = 0.1
-
begin
-
File.open(@filename, File::WRONLY | File::APPEND) do |lock|
-
lock.flock(File::LOCK_EX) # inter-process locking. will be unlocked at closing file
-
if File.identical?(@filename, lock) and File.identical?(lock, @dev)
-
yield # log shifting
-
else
-
# log shifted by another process (i-node before locking and i-node after locking are different)
-
@dev.close rescue nil
-
@dev = open_logfile(@filename)
-
@dev.sync = true
-
end
-
end
-
rescue Errno::ENOENT
-
# @filename file would not exist right after #rename and before #create_logfile
-
if retry_limit <= 0
-
warn("log rotation inter-process lock failed. #{$!}")
-
else
-
sleep retry_sleep
-
retry_limit -= 1
-
retry_sleep *= 2
-
retry
-
end
-
end
-
rescue
-
warn("log rotation inter-process lock failed. #{$!}")
-
end
-
end
-
-
1
def shift_log_age
-
(@shift_age-3).downto(0) do |i|
-
if FileTest.exist?("#{@filename}.#{i}")
-
File.rename("#{@filename}.#{i}", "#{@filename}.#{i+1}")
-
end
-
end
-
@dev.close rescue nil
-
File.rename("#{@filename}", "#{@filename}.0")
-
@dev = create_logfile(@filename)
-
return true
-
end
-
-
1
def shift_log_period(period_end)
-
suffix = period_end.strftime(@shift_period_suffix)
-
age_file = "#{@filename}.#{suffix}"
-
if FileTest.exist?(age_file)
-
# try to avoid filename crash caused by Timestamp change.
-
idx = 0
-
# .99 can be overridden; avoid too much file search with 'loop do'
-
while idx < 100
-
idx += 1
-
age_file = "#{@filename}.#{suffix}.#{idx}"
-
break unless FileTest.exist?(age_file)
-
end
-
end
-
@dev.close rescue nil
-
File.rename("#{@filename}", age_file)
-
@dev = create_logfile(@filename)
-
return true
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
class Logger
-
1
module Period
-
1
module_function
-
-
1
SiD = 24 * 60 * 60
-
-
1
def next_rotate_time(now, shift_age)
-
case shift_age
-
when 'daily'
-
t = Time.mktime(now.year, now.month, now.mday) + SiD
-
when 'weekly'
-
t = Time.mktime(now.year, now.month, now.mday) + SiD * (7 - now.wday)
-
when 'monthly'
-
t = Time.mktime(now.year, now.month, 1) + SiD * 32
-
return Time.mktime(t.year, t.month, 1)
-
when 'now', 'everytime'
-
return now
-
else
-
raise ArgumentError, "invalid :shift_age #{shift_age.inspect}, should be daily, weekly, monthly, or everytime"
-
end
-
if t.hour.nonzero? or t.min.nonzero? or t.sec.nonzero?
-
hour = t.hour
-
t = Time.mktime(t.year, t.month, t.mday)
-
t += SiD if hour > 12
-
end
-
t
-
end
-
-
1
def previous_period_end(now, shift_age)
-
case shift_age
-
when 'daily'
-
t = Time.mktime(now.year, now.month, now.mday) - SiD / 2
-
when 'weekly'
-
t = Time.mktime(now.year, now.month, now.mday) - (SiD * now.wday + SiD / 2)
-
when 'monthly'
-
t = Time.mktime(now.year, now.month, 1) - SiD / 2
-
when 'now', 'everytime'
-
return now
-
else
-
raise ArgumentError, "invalid :shift_age #{shift_age.inspect}, should be daily, weekly, monthly, or everytime"
-
end
-
Time.mktime(t.year, t.month, t.mday, 23, 59, 59)
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
class Logger
-
# Logging severity.
-
1
module Severity
-
# Low-level information, mostly for developers.
-
1
DEBUG = 0
-
# Generic (useful) information about system operation.
-
1
INFO = 1
-
# A warning.
-
1
WARN = 2
-
# A handleable error condition.
-
1
ERROR = 3
-
# An unhandleable error that results in a program crash.
-
1
FATAL = 4
-
# An unknown message that should always be logged.
-
1
UNKNOWN = 5
-
end
-
end
-
# frozen_string_literal: true
-
-
1
class Logger
-
1
VERSION = "1.4.2"
-
end
-
# frozen_string_literal: false
-
#
-
# = net/http.rb
-
#
-
# Copyright (c) 1999-2007 Yukihiro Matsumoto
-
# Copyright (c) 1999-2007 Minero Aoki
-
# Copyright (c) 2001 GOTOU Yuuzou
-
#
-
# Written and maintained by Minero Aoki <aamine@loveruby.net>.
-
# HTTPS support added by GOTOU Yuuzou <gotoyuzo@notwork.org>.
-
#
-
# This file is derived from "http-access.rb".
-
#
-
# Documented by Minero Aoki; converted to RDoc by William Webber.
-
#
-
# This program is free software. You can re-distribute and/or
-
# modify this program under the same terms of ruby itself ---
-
# Ruby Distribution License or GNU General Public License.
-
#
-
# See Net::HTTP for an overview and examples.
-
#
-
-
1
require_relative 'protocol'
-
1
require 'uri'
-
1
autoload :OpenSSL, 'openssl'
-
-
1
module Net #:nodoc:
-
-
# :stopdoc:
-
1
class HTTPBadResponse < StandardError; end
-
1
class HTTPHeaderSyntaxError < StandardError; end
-
# :startdoc:
-
-
# == An HTTP client API for Ruby.
-
#
-
# Net::HTTP provides a rich library which can be used to build HTTP
-
# user-agents. For more details about HTTP see
-
# [RFC2616](http://www.ietf.org/rfc/rfc2616.txt).
-
#
-
# Net::HTTP is designed to work closely with URI. URI::HTTP#host,
-
# URI::HTTP#port and URI::HTTP#request_uri are designed to work with
-
# Net::HTTP.
-
#
-
# If you are only performing a few GET requests you should try OpenURI.
-
#
-
# == Simple Examples
-
#
-
# All examples assume you have loaded Net::HTTP with:
-
#
-
# require 'net/http'
-
#
-
# This will also require 'uri' so you don't need to require it separately.
-
#
-
# The Net::HTTP methods in the following section do not persist
-
# connections. They are not recommended if you are performing many HTTP
-
# requests.
-
#
-
# === GET
-
#
-
# Net::HTTP.get('example.com', '/index.html') # => String
-
#
-
# === GET by URI
-
#
-
# uri = URI('http://example.com/index.html?count=10')
-
# Net::HTTP.get(uri) # => String
-
#
-
# === GET with Dynamic Parameters
-
#
-
# uri = URI('http://example.com/index.html')
-
# params = { :limit => 10, :page => 3 }
-
# uri.query = URI.encode_www_form(params)
-
#
-
# res = Net::HTTP.get_response(uri)
-
# puts res.body if res.is_a?(Net::HTTPSuccess)
-
#
-
# === POST
-
#
-
# uri = URI('http://www.example.com/search.cgi')
-
# res = Net::HTTP.post_form(uri, 'q' => 'ruby', 'max' => '50')
-
# puts res.body
-
#
-
# === POST with Multiple Values
-
#
-
# uri = URI('http://www.example.com/search.cgi')
-
# res = Net::HTTP.post_form(uri, 'q' => ['ruby', 'perl'], 'max' => '50')
-
# puts res.body
-
#
-
# == How to use Net::HTTP
-
#
-
# The following example code can be used as the basis of an HTTP user-agent
-
# which can perform a variety of request types using persistent
-
# connections.
-
#
-
# uri = URI('http://example.com/some_path?query=string')
-
#
-
# Net::HTTP.start(uri.host, uri.port) do |http|
-
# request = Net::HTTP::Get.new uri
-
#
-
# response = http.request request # Net::HTTPResponse object
-
# end
-
#
-
# Net::HTTP::start immediately creates a connection to an HTTP server which
-
# is kept open for the duration of the block. The connection will remain
-
# open for multiple requests in the block if the server indicates it
-
# supports persistent connections.
-
#
-
# If you wish to re-use a connection across multiple HTTP requests without
-
# automatically closing it you can use ::new and then call #start and
-
# #finish manually.
-
#
-
# The request types Net::HTTP supports are listed below in the section "HTTP
-
# Request Classes".
-
#
-
# For all the Net::HTTP request objects and shortcut request methods you may
-
# supply either a String for the request path or a URI from which Net::HTTP
-
# will extract the request path.
-
#
-
# === Response Data
-
#
-
# uri = URI('http://example.com/index.html')
-
# res = Net::HTTP.get_response(uri)
-
#
-
# # Headers
-
# res['Set-Cookie'] # => String
-
# res.get_fields('set-cookie') # => Array
-
# res.to_hash['set-cookie'] # => Array
-
# puts "Headers: #{res.to_hash.inspect}"
-
#
-
# # Status
-
# puts res.code # => '200'
-
# puts res.message # => 'OK'
-
# puts res.class.name # => 'HTTPOK'
-
#
-
# # Body
-
# puts res.body if res.response_body_permitted?
-
#
-
# === Following Redirection
-
#
-
# Each Net::HTTPResponse object belongs to a class for its response code.
-
#
-
# For example, all 2XX responses are instances of a Net::HTTPSuccess
-
# subclass, a 3XX response is an instance of a Net::HTTPRedirection
-
# subclass and a 200 response is an instance of the Net::HTTPOK class. For
-
# details of response classes, see the section "HTTP Response Classes"
-
# below.
-
#
-
# Using a case statement you can handle various types of responses properly:
-
#
-
# def fetch(uri_str, limit = 10)
-
# # You should choose a better exception.
-
# raise ArgumentError, 'too many HTTP redirects' if limit == 0
-
#
-
# response = Net::HTTP.get_response(URI(uri_str))
-
#
-
# case response
-
# when Net::HTTPSuccess then
-
# response
-
# when Net::HTTPRedirection then
-
# location = response['location']
-
# warn "redirected to #{location}"
-
# fetch(location, limit - 1)
-
# else
-
# response.value
-
# end
-
# end
-
#
-
# print fetch('http://www.ruby-lang.org')
-
#
-
# === POST
-
#
-
# A POST can be made using the Net::HTTP::Post request class. This example
-
# creates a URL encoded POST body:
-
#
-
# uri = URI('http://www.example.com/todo.cgi')
-
# req = Net::HTTP::Post.new(uri)
-
# req.set_form_data('from' => '2005-01-01', 'to' => '2005-03-31')
-
#
-
# res = Net::HTTP.start(uri.hostname, uri.port) do |http|
-
# http.request(req)
-
# end
-
#
-
# case res
-
# when Net::HTTPSuccess, Net::HTTPRedirection
-
# # OK
-
# else
-
# res.value
-
# end
-
#
-
# To send multipart/form-data use Net::HTTPHeader#set_form:
-
#
-
# req = Net::HTTP::Post.new(uri)
-
# req.set_form([['upload', File.open('foo.bar')]], 'multipart/form-data')
-
#
-
# Other requests that can contain a body such as PUT can be created in the
-
# same way using the corresponding request class (Net::HTTP::Put).
-
#
-
# === Setting Headers
-
#
-
# The following example performs a conditional GET using the
-
# If-Modified-Since header. If the files has not been modified since the
-
# time in the header a Not Modified response will be returned. See RFC 2616
-
# section 9.3 for further details.
-
#
-
# uri = URI('http://example.com/cached_response')
-
# file = File.stat 'cached_response'
-
#
-
# req = Net::HTTP::Get.new(uri)
-
# req['If-Modified-Since'] = file.mtime.rfc2822
-
#
-
# res = Net::HTTP.start(uri.hostname, uri.port) {|http|
-
# http.request(req)
-
# }
-
#
-
# open 'cached_response', 'w' do |io|
-
# io.write res.body
-
# end if res.is_a?(Net::HTTPSuccess)
-
#
-
# === Basic Authentication
-
#
-
# Basic authentication is performed according to
-
# [RFC2617](http://www.ietf.org/rfc/rfc2617.txt).
-
#
-
# uri = URI('http://example.com/index.html?key=value')
-
#
-
# req = Net::HTTP::Get.new(uri)
-
# req.basic_auth 'user', 'pass'
-
#
-
# res = Net::HTTP.start(uri.hostname, uri.port) {|http|
-
# http.request(req)
-
# }
-
# puts res.body
-
#
-
# === Streaming Response Bodies
-
#
-
# By default Net::HTTP reads an entire response into memory. If you are
-
# handling large files or wish to implement a progress bar you can instead
-
# stream the body directly to an IO.
-
#
-
# uri = URI('http://example.com/large_file')
-
#
-
# Net::HTTP.start(uri.host, uri.port) do |http|
-
# request = Net::HTTP::Get.new uri
-
#
-
# http.request request do |response|
-
# open 'large_file', 'w' do |io|
-
# response.read_body do |chunk|
-
# io.write chunk
-
# end
-
# end
-
# end
-
# end
-
#
-
# === HTTPS
-
#
-
# HTTPS is enabled for an HTTP connection by Net::HTTP#use_ssl=.
-
#
-
# uri = URI('https://secure.example.com/some_path?query=string')
-
#
-
# Net::HTTP.start(uri.host, uri.port, :use_ssl => true) do |http|
-
# request = Net::HTTP::Get.new uri
-
# response = http.request request # Net::HTTPResponse object
-
# end
-
#
-
# Or if you simply want to make a GET request, you may pass in an URI
-
# object that has an HTTPS URL. Net::HTTP automatically turns on TLS
-
# verification if the URI object has a 'https' URI scheme.
-
#
-
# uri = URI('https://example.com/')
-
# Net::HTTP.get(uri) # => String
-
#
-
# In previous versions of Ruby you would need to require 'net/https' to use
-
# HTTPS. This is no longer true.
-
#
-
# === Proxies
-
#
-
# Net::HTTP will automatically create a proxy from the +http_proxy+
-
# environment variable if it is present. To disable use of +http_proxy+,
-
# pass +nil+ for the proxy address.
-
#
-
# You may also create a custom proxy:
-
#
-
# proxy_addr = 'your.proxy.host'
-
# proxy_port = 8080
-
#
-
# Net::HTTP.new('example.com', nil, proxy_addr, proxy_port).start { |http|
-
# # always proxy via your.proxy.addr:8080
-
# }
-
#
-
# See Net::HTTP.new for further details and examples such as proxies that
-
# require a username and password.
-
#
-
# === Compression
-
#
-
# Net::HTTP automatically adds Accept-Encoding for compression of response
-
# bodies and automatically decompresses gzip and deflate responses unless a
-
# Range header was sent.
-
#
-
# Compression can be disabled through the Accept-Encoding: identity header.
-
#
-
# == HTTP Request Classes
-
#
-
# Here is the HTTP request class hierarchy.
-
#
-
# * Net::HTTPRequest
-
# * Net::HTTP::Get
-
# * Net::HTTP::Head
-
# * Net::HTTP::Post
-
# * Net::HTTP::Patch
-
# * Net::HTTP::Put
-
# * Net::HTTP::Proppatch
-
# * Net::HTTP::Lock
-
# * Net::HTTP::Unlock
-
# * Net::HTTP::Options
-
# * Net::HTTP::Propfind
-
# * Net::HTTP::Delete
-
# * Net::HTTP::Move
-
# * Net::HTTP::Copy
-
# * Net::HTTP::Mkcol
-
# * Net::HTTP::Trace
-
#
-
# == HTTP Response Classes
-
#
-
# Here is HTTP response class hierarchy. All classes are defined in Net
-
# module and are subclasses of Net::HTTPResponse.
-
#
-
# HTTPUnknownResponse:: For unhandled HTTP extensions
-
# HTTPInformation:: 1xx
-
# HTTPContinue:: 100
-
# HTTPSwitchProtocol:: 101
-
# HTTPSuccess:: 2xx
-
# HTTPOK:: 200
-
# HTTPCreated:: 201
-
# HTTPAccepted:: 202
-
# HTTPNonAuthoritativeInformation:: 203
-
# HTTPNoContent:: 204
-
# HTTPResetContent:: 205
-
# HTTPPartialContent:: 206
-
# HTTPMultiStatus:: 207
-
# HTTPIMUsed:: 226
-
# HTTPRedirection:: 3xx
-
# HTTPMultipleChoices:: 300
-
# HTTPMovedPermanently:: 301
-
# HTTPFound:: 302
-
# HTTPSeeOther:: 303
-
# HTTPNotModified:: 304
-
# HTTPUseProxy:: 305
-
# HTTPTemporaryRedirect:: 307
-
# HTTPClientError:: 4xx
-
# HTTPBadRequest:: 400
-
# HTTPUnauthorized:: 401
-
# HTTPPaymentRequired:: 402
-
# HTTPForbidden:: 403
-
# HTTPNotFound:: 404
-
# HTTPMethodNotAllowed:: 405
-
# HTTPNotAcceptable:: 406
-
# HTTPProxyAuthenticationRequired:: 407
-
# HTTPRequestTimeOut:: 408
-
# HTTPConflict:: 409
-
# HTTPGone:: 410
-
# HTTPLengthRequired:: 411
-
# HTTPPreconditionFailed:: 412
-
# HTTPRequestEntityTooLarge:: 413
-
# HTTPRequestURITooLong:: 414
-
# HTTPUnsupportedMediaType:: 415
-
# HTTPRequestedRangeNotSatisfiable:: 416
-
# HTTPExpectationFailed:: 417
-
# HTTPUnprocessableEntity:: 422
-
# HTTPLocked:: 423
-
# HTTPFailedDependency:: 424
-
# HTTPUpgradeRequired:: 426
-
# HTTPPreconditionRequired:: 428
-
# HTTPTooManyRequests:: 429
-
# HTTPRequestHeaderFieldsTooLarge:: 431
-
# HTTPUnavailableForLegalReasons:: 451
-
# HTTPServerError:: 5xx
-
# HTTPInternalServerError:: 500
-
# HTTPNotImplemented:: 501
-
# HTTPBadGateway:: 502
-
# HTTPServiceUnavailable:: 503
-
# HTTPGatewayTimeOut:: 504
-
# HTTPVersionNotSupported:: 505
-
# HTTPInsufficientStorage:: 507
-
# HTTPNetworkAuthenticationRequired:: 511
-
#
-
# There is also the Net::HTTPBadResponse exception which is raised when
-
# there is a protocol error.
-
#
-
1
class HTTP < Protocol
-
-
# :stopdoc:
-
1
Revision = %q$Revision$.split[1]
-
1
HTTPVersion = '1.1'
-
begin
-
1
require 'zlib'
-
1
require 'stringio' #for our purposes (unpacking gzip) lump these together
-
1
HAVE_ZLIB=true
-
rescue LoadError
-
HAVE_ZLIB=false
-
end
-
# :startdoc:
-
-
# Turns on net/http 1.2 (Ruby 1.8) features.
-
# Defaults to ON in Ruby 1.8 or later.
-
1
def HTTP.version_1_2
-
true
-
end
-
-
# Returns true if net/http is in version 1.2 mode.
-
# Defaults to true.
-
1
def HTTP.version_1_2?
-
true
-
end
-
-
1
def HTTP.version_1_1? #:nodoc:
-
false
-
end
-
-
1
class << HTTP
-
1
alias is_version_1_1? version_1_1? #:nodoc:
-
1
alias is_version_1_2? version_1_2? #:nodoc:
-
end
-
-
#
-
# short cut methods
-
#
-
-
#
-
# Gets the body text from the target and outputs it to $stdout. The
-
# target can either be specified as
-
# (+uri+), or as (+host+, +path+, +port+ = 80); so:
-
#
-
# Net::HTTP.get_print URI('http://www.example.com/index.html')
-
#
-
# or:
-
#
-
# Net::HTTP.get_print 'www.example.com', '/index.html'
-
#
-
1
def HTTP.get_print(uri_or_host, path = nil, port = nil)
-
get_response(uri_or_host, path, port) {|res|
-
res.read_body do |chunk|
-
$stdout.print chunk
-
end
-
}
-
nil
-
end
-
-
# Sends a GET request to the target and returns the HTTP response
-
# as a string. The target can either be specified as
-
# (+uri+), or as (+host+, +path+, +port+ = 80); so:
-
#
-
# print Net::HTTP.get(URI('http://www.example.com/index.html'))
-
#
-
# or:
-
#
-
# print Net::HTTP.get('www.example.com', '/index.html')
-
#
-
1
def HTTP.get(uri_or_host, path = nil, port = nil)
-
get_response(uri_or_host, path, port).body
-
end
-
-
# Sends a GET request to the target and returns the HTTP response
-
# as a Net::HTTPResponse object. The target can either be specified as
-
# (+uri+), or as (+host+, +path+, +port+ = 80); so:
-
#
-
# res = Net::HTTP.get_response(URI('http://www.example.com/index.html'))
-
# print res.body
-
#
-
# or:
-
#
-
# res = Net::HTTP.get_response('www.example.com', '/index.html')
-
# print res.body
-
#
-
1
def HTTP.get_response(uri_or_host, path = nil, port = nil, &block)
-
if path
-
host = uri_or_host
-
new(host, port || HTTP.default_port).start {|http|
-
return http.request_get(path, &block)
-
}
-
else
-
uri = uri_or_host
-
start(uri.hostname, uri.port,
-
:use_ssl => uri.scheme == 'https') {|http|
-
return http.request_get(uri, &block)
-
}
-
end
-
end
-
-
# Posts data to the specified URI object.
-
#
-
# Example:
-
#
-
# require 'net/http'
-
# require 'uri'
-
#
-
# Net::HTTP.post URI('http://www.example.com/api/search'),
-
# { "q" => "ruby", "max" => "50" }.to_json,
-
# "Content-Type" => "application/json"
-
#
-
1
def HTTP.post(url, data, header = nil)
-
start(url.hostname, url.port,
-
:use_ssl => url.scheme == 'https' ) {|http|
-
http.post(url, data, header)
-
}
-
end
-
-
# Posts HTML form data to the specified URI object.
-
# The form data must be provided as a Hash mapping from String to String.
-
# Example:
-
#
-
# { "cmd" => "search", "q" => "ruby", "max" => "50" }
-
#
-
# This method also does Basic Authentication iff +url+.user exists.
-
# But userinfo for authentication is deprecated (RFC3986).
-
# So this feature will be removed.
-
#
-
# Example:
-
#
-
# require 'net/http'
-
# require 'uri'
-
#
-
# Net::HTTP.post_form URI('http://www.example.com/search.cgi'),
-
# { "q" => "ruby", "max" => "50" }
-
#
-
1
def HTTP.post_form(url, params)
-
req = Post.new(url)
-
req.form_data = params
-
req.basic_auth url.user, url.password if url.user
-
start(url.hostname, url.port,
-
:use_ssl => url.scheme == 'https' ) {|http|
-
http.request(req)
-
}
-
end
-
-
#
-
# HTTP session management
-
#
-
-
# The default port to use for HTTP requests; defaults to 80.
-
1
def HTTP.default_port
-
http_default_port()
-
end
-
-
# The default port to use for HTTP requests; defaults to 80.
-
1
def HTTP.http_default_port
-
80
-
end
-
-
# The default port to use for HTTPS requests; defaults to 443.
-
1
def HTTP.https_default_port
-
443
-
end
-
-
1
def HTTP.socket_type #:nodoc: obsolete
-
BufferedIO
-
end
-
-
# :call-seq:
-
# HTTP.start(address, port, p_addr, p_port, p_user, p_pass, &block)
-
# HTTP.start(address, port=nil, p_addr=:ENV, p_port=nil, p_user=nil, p_pass=nil, opt, &block)
-
#
-
# Creates a new Net::HTTP object, then additionally opens the TCP
-
# connection and HTTP session.
-
#
-
# Arguments are the following:
-
# _address_ :: hostname or IP address of the server
-
# _port_ :: port of the server
-
# _p_addr_ :: address of proxy
-
# _p_port_ :: port of proxy
-
# _p_user_ :: user of proxy
-
# _p_pass_ :: pass of proxy
-
# _opt_ :: optional hash
-
#
-
# _opt_ sets following values by its accessor.
-
# The keys are ipaddr, ca_file, ca_path, cert, cert_store, ciphers,
-
# close_on_empty_response, key, open_timeout, read_timeout, write_timeout, ssl_timeout,
-
# ssl_version, use_ssl, verify_callback, verify_depth and verify_mode.
-
# If you set :use_ssl as true, you can use https and default value of
-
# verify_mode is set as OpenSSL::SSL::VERIFY_PEER.
-
#
-
# If the optional block is given, the newly
-
# created Net::HTTP object is passed to it and closed when the
-
# block finishes. In this case, the return value of this method
-
# is the return value of the block. If no block is given, the
-
# return value of this method is the newly created Net::HTTP object
-
# itself, and the caller is responsible for closing it upon completion
-
# using the finish() method.
-
1
def HTTP.start(address, *arg, &block) # :yield: +http+
-
3
arg.pop if opt = Hash.try_convert(arg[-1])
-
3
port, p_addr, p_port, p_user, p_pass = *arg
-
3
p_addr = :ENV if arg.size < 2
-
3
port = https_default_port if !port && opt && opt[:use_ssl]
-
3
http = new(address, port, p_addr, p_port, p_user, p_pass)
-
3
http.ipaddr = opt[:ipaddr] if opt && opt[:ipaddr]
-
-
3
if opt
-
if opt[:use_ssl]
-
opt = {verify_mode: OpenSSL::SSL::VERIFY_PEER}.update(opt)
-
end
-
http.methods.grep(/\A(\w+)=\z/) do |meth|
-
key = $1.to_sym
-
opt.key?(key) or next
-
http.__send__(meth, opt[key])
-
end
-
end
-
-
3
http.start(&block)
-
end
-
-
1
class << HTTP
-
1
alias newobj new # :nodoc:
-
end
-
-
# Creates a new Net::HTTP object without opening a TCP connection or
-
# HTTP session.
-
#
-
# The +address+ should be a DNS hostname or IP address, the +port+ is the
-
# port the server operates on. If no +port+ is given the default port for
-
# HTTP or HTTPS is used.
-
#
-
# If none of the +p_+ arguments are given, the proxy host and port are
-
# taken from the +http_proxy+ environment variable (or its uppercase
-
# equivalent) if present. If the proxy requires authentication you must
-
# supply it by hand. See URI::Generic#find_proxy for details of proxy
-
# detection from the environment. To disable proxy detection set +p_addr+
-
# to nil.
-
#
-
# If you are connecting to a custom proxy, +p_addr+ specifies the DNS name
-
# or IP address of the proxy host, +p_port+ the port to use to access the
-
# proxy, +p_user+ and +p_pass+ the username and password if authorization
-
# is required to use the proxy, and p_no_proxy hosts which do not
-
# use the proxy.
-
#
-
1
def HTTP.new(address, port = nil, p_addr = :ENV, p_port = nil, p_user = nil, p_pass = nil, p_no_proxy = nil)
-
3
http = super address, port
-
-
3
if proxy_class? then # from Net::HTTP::Proxy()
-
http.proxy_from_env = @proxy_from_env
-
http.proxy_address = @proxy_address
-
http.proxy_port = @proxy_port
-
http.proxy_user = @proxy_user
-
http.proxy_pass = @proxy_pass
-
3
elsif p_addr == :ENV then
-
3
http.proxy_from_env = true
-
else
-
if p_addr && p_no_proxy && !URI::Generic.use_proxy?(p_addr, p_addr, p_port, p_no_proxy)
-
p_addr = nil
-
p_port = nil
-
end
-
http.proxy_address = p_addr
-
http.proxy_port = p_port || default_port
-
http.proxy_user = p_user
-
http.proxy_pass = p_pass
-
end
-
-
3
http
-
end
-
-
# Creates a new Net::HTTP object for the specified server address,
-
# without opening the TCP connection or initializing the HTTP session.
-
# The +address+ should be a DNS hostname or IP address.
-
1
def initialize(address, port = nil)
-
3
@address = address
-
3
@port = (port || HTTP.default_port)
-
3
@ipaddr = nil
-
3
@local_host = nil
-
3
@local_port = nil
-
3
@curr_http_version = HTTPVersion
-
3
@keep_alive_timeout = 2
-
3
@last_communicated = nil
-
3
@close_on_empty_response = false
-
3
@socket = nil
-
3
@started = false
-
3
@open_timeout = 60
-
3
@read_timeout = 60
-
3
@write_timeout = 60
-
3
@continue_timeout = nil
-
3
@max_retries = 1
-
3
@debug_output = nil
-
-
3
@proxy_from_env = false
-
3
@proxy_uri = nil
-
3
@proxy_address = nil
-
3
@proxy_port = nil
-
3
@proxy_user = nil
-
3
@proxy_pass = nil
-
-
3
@use_ssl = false
-
3
@ssl_context = nil
-
3
@ssl_session = nil
-
3
@sspi_enabled = false
-
3
SSL_IVNAMES.each do |ivname|
-
39
instance_variable_set ivname, nil
-
end
-
end
-
-
1
def inspect
-
"#<#{self.class} #{@address}:#{@port} open=#{started?}>"
-
end
-
-
# *WARNING* This method opens a serious security hole.
-
# Never use this method in production code.
-
#
-
# Sets an output stream for debugging.
-
#
-
# http = Net::HTTP.new(hostname)
-
# http.set_debug_output $stderr
-
# http.start { .... }
-
#
-
1
def set_debug_output(output)
-
warn 'Net::HTTP#set_debug_output called after HTTP started', uplevel: 1 if started?
-
@debug_output = output
-
end
-
-
# The DNS host name or IP address to connect to.
-
1
attr_reader :address
-
-
# The port number to connect to.
-
1
attr_reader :port
-
-
# The local host used to establish the connection.
-
1
attr_accessor :local_host
-
-
# The local port used to establish the connection.
-
1
attr_accessor :local_port
-
-
1
attr_writer :proxy_from_env
-
1
attr_writer :proxy_address
-
1
attr_writer :proxy_port
-
1
attr_writer :proxy_user
-
1
attr_writer :proxy_pass
-
-
# The IP address to connect to/used to connect to
-
1
def ipaddr
-
started? ? @socket.io.peeraddr[3] : @ipaddr
-
end
-
-
# Set the IP address to connect to
-
1
def ipaddr=(addr)
-
raise IOError, "ipaddr value changed, but session already started" if started?
-
@ipaddr = addr
-
end
-
-
# Number of seconds to wait for the connection to open. Any number
-
# may be used, including Floats for fractional seconds. If the HTTP
-
# object cannot open a connection in this many seconds, it raises a
-
# Net::OpenTimeout exception. The default value is 60 seconds.
-
1
attr_accessor :open_timeout
-
-
# Number of seconds to wait for one block to be read (via one read(2)
-
# call). Any number may be used, including Floats for fractional
-
# seconds. If the HTTP object cannot read data in this many seconds,
-
# it raises a Net::ReadTimeout exception. The default value is 60 seconds.
-
1
attr_reader :read_timeout
-
-
# Number of seconds to wait for one block to be written (via one write(2)
-
# call). Any number may be used, including Floats for fractional
-
# seconds. If the HTTP object cannot write data in this many seconds,
-
# it raises a Net::WriteTimeout exception. The default value is 60 seconds.
-
# Net::WriteTimeout is not raised on Windows.
-
1
attr_reader :write_timeout
-
-
# Maximum number of times to retry an idempotent request in case of
-
# Net::ReadTimeout, IOError, EOFError, Errno::ECONNRESET,
-
# Errno::ECONNABORTED, Errno::EPIPE, OpenSSL::SSL::SSLError,
-
# Timeout::Error.
-
# Should be a non-negative integer number. Zero means no retries.
-
# The default value is 1.
-
1
def max_retries=(retries)
-
retries = retries.to_int
-
if retries < 0
-
raise ArgumentError, 'max_retries should be non-negative integer number'
-
end
-
@max_retries = retries
-
end
-
-
1
attr_reader :max_retries
-
-
# Setter for the read_timeout attribute.
-
1
def read_timeout=(sec)
-
@socket.read_timeout = sec if @socket
-
@read_timeout = sec
-
end
-
-
# Setter for the write_timeout attribute.
-
1
def write_timeout=(sec)
-
@socket.write_timeout = sec if @socket
-
@write_timeout = sec
-
end
-
-
# Seconds to wait for 100 Continue response. If the HTTP object does not
-
# receive a response in this many seconds it sends the request body. The
-
# default value is +nil+.
-
1
attr_reader :continue_timeout
-
-
# Setter for the continue_timeout attribute.
-
1
def continue_timeout=(sec)
-
@socket.continue_timeout = sec if @socket
-
@continue_timeout = sec
-
end
-
-
# Seconds to reuse the connection of the previous request.
-
# If the idle time is less than this Keep-Alive Timeout,
-
# Net::HTTP reuses the TCP/IP socket used by the previous communication.
-
# The default value is 2 seconds.
-
1
attr_accessor :keep_alive_timeout
-
-
# Returns true if the HTTP session has been started.
-
1
def started?
-
3
@started
-
end
-
-
1
alias active? started? #:nodoc: obsolete
-
-
1
attr_accessor :close_on_empty_response
-
-
# Returns true if SSL/TLS is being used with HTTP.
-
1
def use_ssl?
-
6
@use_ssl
-
end
-
-
# Turn on/off SSL.
-
# This flag must be set before starting session.
-
# If you change use_ssl value after session started,
-
# a Net::HTTP object raises IOError.
-
1
def use_ssl=(flag)
-
flag = flag ? true : false
-
if started? and @use_ssl != flag
-
raise IOError, "use_ssl value changed, but session already started"
-
end
-
@use_ssl = flag
-
end
-
-
1
SSL_IVNAMES = [
-
:@ca_file,
-
:@ca_path,
-
:@cert,
-
:@cert_store,
-
:@ciphers,
-
:@key,
-
:@ssl_timeout,
-
:@ssl_version,
-
:@min_version,
-
:@max_version,
-
:@verify_callback,
-
:@verify_depth,
-
:@verify_mode,
-
]
-
1
SSL_ATTRIBUTES = [
-
:ca_file,
-
:ca_path,
-
:cert,
-
:cert_store,
-
:ciphers,
-
:key,
-
:ssl_timeout,
-
:ssl_version,
-
:min_version,
-
:max_version,
-
:verify_callback,
-
:verify_depth,
-
:verify_mode,
-
]
-
-
# Sets path of a CA certification file in PEM format.
-
#
-
# The file can contain several CA certificates.
-
1
attr_accessor :ca_file
-
-
# Sets path of a CA certification directory containing certifications in
-
# PEM format.
-
1
attr_accessor :ca_path
-
-
# Sets an OpenSSL::X509::Certificate object as client certificate.
-
# (This method is appeared in Michal Rokos's OpenSSL extension).
-
1
attr_accessor :cert
-
-
# Sets the X509::Store to verify peer certificate.
-
1
attr_accessor :cert_store
-
-
# Sets the available ciphers. See OpenSSL::SSL::SSLContext#ciphers=
-
1
attr_accessor :ciphers
-
-
# Sets an OpenSSL::PKey::RSA or OpenSSL::PKey::DSA object.
-
# (This method is appeared in Michal Rokos's OpenSSL extension.)
-
1
attr_accessor :key
-
-
# Sets the SSL timeout seconds.
-
1
attr_accessor :ssl_timeout
-
-
# Sets the SSL version. See OpenSSL::SSL::SSLContext#ssl_version=
-
1
attr_accessor :ssl_version
-
-
# Sets the minimum SSL version. See OpenSSL::SSL::SSLContext#min_version=
-
1
attr_accessor :min_version
-
-
# Sets the maximum SSL version. See OpenSSL::SSL::SSLContext#max_version=
-
1
attr_accessor :max_version
-
-
# Sets the verify callback for the server certification verification.
-
1
attr_accessor :verify_callback
-
-
# Sets the maximum depth for the certificate chain verification.
-
1
attr_accessor :verify_depth
-
-
# Sets the flags for server the certification verification at beginning of
-
# SSL/TLS session.
-
#
-
# OpenSSL::SSL::VERIFY_NONE or OpenSSL::SSL::VERIFY_PEER are acceptable.
-
1
attr_accessor :verify_mode
-
-
# Returns the X.509 certificates the server presented.
-
1
def peer_cert
-
if not use_ssl? or not @socket
-
return nil
-
end
-
@socket.io.peer_cert
-
end
-
-
# Opens a TCP connection and HTTP session.
-
#
-
# When this method is called with a block, it passes the Net::HTTP
-
# object to the block, and closes the TCP connection and HTTP session
-
# after the block has been executed.
-
#
-
# When called with a block, it returns the return value of the
-
# block; otherwise, it returns self.
-
#
-
1
def start # :yield: http
-
3
raise IOError, 'HTTP session already opened' if @started
-
3
if block_given?
-
begin
-
3
do_start
-
3
return yield(self)
-
ensure
-
3
do_finish
-
end
-
end
-
do_start
-
self
-
end
-
-
1
def do_start
-
3
connect
-
3
@started = true
-
end
-
1
private :do_start
-
-
1
def connect
-
3
if proxy? then
-
conn_addr = proxy_address
-
conn_port = proxy_port
-
else
-
3
conn_addr = conn_address
-
3
conn_port = port
-
end
-
-
3
D "opening connection to #{conn_addr}:#{conn_port}..."
-
3
s = Timeout.timeout(@open_timeout, Net::OpenTimeout) {
-
begin
-
3
TCPSocket.open(conn_addr, conn_port, @local_host, @local_port)
-
rescue => e
-
raise e, "Failed to open TCP connection to " +
-
"#{conn_addr}:#{conn_port} (#{e.message})"
-
end
-
}
-
3
s.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
-
3
D "opened"
-
3
if use_ssl?
-
if proxy?
-
plain_sock = BufferedIO.new(s, read_timeout: @read_timeout,
-
write_timeout: @write_timeout,
-
continue_timeout: @continue_timeout,
-
debug_output: @debug_output)
-
buf = "CONNECT #{conn_address}:#{@port} HTTP/#{HTTPVersion}\r\n"
-
buf << "Host: #{@address}:#{@port}\r\n"
-
if proxy_user
-
credential = ["#{proxy_user}:#{proxy_pass}"].pack('m0')
-
buf << "Proxy-Authorization: Basic #{credential}\r\n"
-
end
-
buf << "\r\n"
-
plain_sock.write(buf)
-
HTTPResponse.read_new(plain_sock).value
-
# assuming nothing left in buffers after successful CONNECT response
-
end
-
-
ssl_parameters = Hash.new
-
iv_list = instance_variables
-
SSL_IVNAMES.each_with_index do |ivname, i|
-
if iv_list.include?(ivname) and
-
value = instance_variable_get(ivname)
-
ssl_parameters[SSL_ATTRIBUTES[i]] = value if value
-
end
-
end
-
@ssl_context = OpenSSL::SSL::SSLContext.new
-
@ssl_context.set_params(ssl_parameters)
-
@ssl_context.session_cache_mode =
-
OpenSSL::SSL::SSLContext::SESSION_CACHE_CLIENT |
-
OpenSSL::SSL::SSLContext::SESSION_CACHE_NO_INTERNAL_STORE
-
@ssl_context.session_new_cb = proc {|sock, sess| @ssl_session = sess }
-
D "starting SSL for #{conn_addr}:#{conn_port}..."
-
s = OpenSSL::SSL::SSLSocket.new(s, @ssl_context)
-
s.sync_close = true
-
# Server Name Indication (SNI) RFC 3546
-
s.hostname = @address if s.respond_to? :hostname=
-
if @ssl_session and
-
Process.clock_gettime(Process::CLOCK_REALTIME) < @ssl_session.time.to_f + @ssl_session.timeout
-
s.session = @ssl_session
-
end
-
ssl_socket_connect(s, @open_timeout)
-
if @ssl_context.verify_mode != OpenSSL::SSL::VERIFY_NONE
-
s.post_connection_check(@address)
-
end
-
D "SSL established, protocol: #{s.ssl_version}, cipher: #{s.cipher[0]}"
-
end
-
3
@socket = BufferedIO.new(s, read_timeout: @read_timeout,
-
write_timeout: @write_timeout,
-
continue_timeout: @continue_timeout,
-
debug_output: @debug_output)
-
3
on_connect
-
rescue => exception
-
if s
-
D "Conn close because of connect error #{exception}"
-
s.close
-
end
-
raise
-
end
-
1
private :connect
-
-
1
def on_connect
-
end
-
1
private :on_connect
-
-
# Finishes the HTTP session and closes the TCP connection.
-
# Raises IOError if the session has not been started.
-
1
def finish
-
raise IOError, 'HTTP session not yet started' unless started?
-
do_finish
-
end
-
-
1
def do_finish
-
3
@started = false
-
3
@socket.close if @socket
-
3
@socket = nil
-
end
-
1
private :do_finish
-
-
#
-
# proxy
-
#
-
-
1
public
-
-
# no proxy
-
1
@is_proxy_class = false
-
1
@proxy_from_env = false
-
1
@proxy_addr = nil
-
1
@proxy_port = nil
-
1
@proxy_user = nil
-
1
@proxy_pass = nil
-
-
# Creates an HTTP proxy class which behaves like Net::HTTP, but
-
# performs all access via the specified proxy.
-
#
-
# This class is obsolete. You may pass these same parameters directly to
-
# Net::HTTP.new. See Net::HTTP.new for details of the arguments.
-
1
def HTTP.Proxy(p_addr = :ENV, p_port = nil, p_user = nil, p_pass = nil)
-
return self unless p_addr
-
-
Class.new(self) {
-
@is_proxy_class = true
-
-
if p_addr == :ENV then
-
@proxy_from_env = true
-
@proxy_address = nil
-
@proxy_port = nil
-
else
-
@proxy_from_env = false
-
@proxy_address = p_addr
-
@proxy_port = p_port || default_port
-
end
-
-
@proxy_user = p_user
-
@proxy_pass = p_pass
-
}
-
end
-
-
1
class << HTTP
-
# returns true if self is a class which was created by HTTP::Proxy.
-
1
def proxy_class?
-
3
defined?(@is_proxy_class) ? @is_proxy_class : false
-
end
-
-
# Address of proxy host. If Net::HTTP does not use a proxy, nil.
-
1
attr_reader :proxy_address
-
-
# Port number of proxy host. If Net::HTTP does not use a proxy, nil.
-
1
attr_reader :proxy_port
-
-
# User name for accessing proxy. If Net::HTTP does not use a proxy, nil.
-
1
attr_reader :proxy_user
-
-
# User password for accessing proxy. If Net::HTTP does not use a proxy,
-
# nil.
-
1
attr_reader :proxy_pass
-
end
-
-
# True if requests for this connection will be proxied
-
1
def proxy?
-
6
!!(@proxy_from_env ? proxy_uri : @proxy_address)
-
end
-
-
# True if the proxy for this connection is determined from the environment
-
1
def proxy_from_env?
-
@proxy_from_env
-
end
-
-
# The proxy URI determined from the environment for this connection.
-
1
def proxy_uri # :nodoc:
-
9
return if @proxy_uri == false
-
3
@proxy_uri ||= URI::HTTP.new(
-
"http".freeze, nil, address, port, nil, nil, nil, nil, nil
-
).find_proxy || false
-
3
@proxy_uri || nil
-
end
-
-
# The address of the proxy server, if one is configured.
-
1
def proxy_address
-
if @proxy_from_env then
-
proxy_uri&.hostname
-
else
-
@proxy_address
-
end
-
end
-
-
# The port of the proxy server, if one is configured.
-
1
def proxy_port
-
if @proxy_from_env then
-
proxy_uri&.port
-
else
-
@proxy_port
-
end
-
end
-
-
# [Bug #12921]
-
1
if /linux|freebsd|darwin/ =~ RUBY_PLATFORM
-
1
ENVIRONMENT_VARIABLE_IS_MULTIUSER_SAFE = true
-
else
-
ENVIRONMENT_VARIABLE_IS_MULTIUSER_SAFE = false
-
end
-
-
# The username of the proxy server, if one is configured.
-
1
def proxy_user
-
3
if ENVIRONMENT_VARIABLE_IS_MULTIUSER_SAFE && @proxy_from_env
-
3
proxy_uri&.user
-
else
-
@proxy_user
-
end
-
end
-
-
# The password of the proxy server, if one is configured.
-
1
def proxy_pass
-
if ENVIRONMENT_VARIABLE_IS_MULTIUSER_SAFE && @proxy_from_env
-
proxy_uri&.password
-
else
-
@proxy_pass
-
end
-
end
-
-
1
alias proxyaddr proxy_address #:nodoc: obsolete
-
1
alias proxyport proxy_port #:nodoc: obsolete
-
-
1
private
-
-
# without proxy, obsolete
-
-
1
def conn_address # :nodoc:
-
3
@ipaddr || address()
-
end
-
-
1
def conn_port # :nodoc:
-
port()
-
end
-
-
1
def edit_path(path)
-
3
if proxy?
-
if path.start_with?("ftp://") || use_ssl?
-
path
-
else
-
"http://#{addr_port}#{path}"
-
end
-
else
-
3
path
-
end
-
end
-
-
#
-
# HTTP operations
-
#
-
-
1
public
-
-
# Retrieves data from +path+ on the connected-to host which may be an
-
# absolute path String or a URI to extract the path from.
-
#
-
# +initheader+ must be a Hash like { 'Accept' => '*/*', ... },
-
# and it defaults to an empty hash.
-
# If +initheader+ doesn't have the key 'accept-encoding', then
-
# a value of "gzip;q=1.0,deflate;q=0.6,identity;q=0.3" is used,
-
# so that gzip compression is used in preference to deflate
-
# compression, which is used in preference to no compression.
-
# Ruby doesn't have libraries to support the compress (Lempel-Ziv)
-
# compression, so that is not supported. The intent of this is
-
# to reduce bandwidth by default. If this routine sets up
-
# compression, then it does the decompression also, removing
-
# the header as well to prevent confusion. Otherwise
-
# it leaves the body as it found it.
-
#
-
# This method returns a Net::HTTPResponse object.
-
#
-
# If called with a block, yields each fragment of the
-
# entity body in turn as a string as it is read from
-
# the socket. Note that in this case, the returned response
-
# object will *not* contain a (meaningful) body.
-
#
-
# +dest+ argument is obsolete.
-
# It still works but you must not use it.
-
#
-
# This method never raises an exception.
-
#
-
# response = http.get('/index.html')
-
#
-
# # using block
-
# File.open('result.txt', 'w') {|f|
-
# http.get('/~foo/') do |str|
-
# f.write str
-
# end
-
# }
-
#
-
1
def get(path, initheader = nil, dest = nil, &block) # :yield: +body_segment+
-
res = nil
-
request(Get.new(path, initheader)) {|r|
-
r.read_body dest, &block
-
res = r
-
}
-
res
-
end
-
-
# Gets only the header from +path+ on the connected-to host.
-
# +header+ is a Hash like { 'Accept' => '*/*', ... }.
-
#
-
# This method returns a Net::HTTPResponse object.
-
#
-
# This method never raises an exception.
-
#
-
# response = nil
-
# Net::HTTP.start('some.www.server', 80) {|http|
-
# response = http.head('/index.html')
-
# }
-
# p response['content-type']
-
#
-
1
def head(path, initheader = nil)
-
request(Head.new(path, initheader))
-
end
-
-
# Posts +data+ (must be a String) to +path+. +header+ must be a Hash
-
# like { 'Accept' => '*/*', ... }.
-
#
-
# This method returns a Net::HTTPResponse object.
-
#
-
# If called with a block, yields each fragment of the
-
# entity body in turn as a string as it is read from
-
# the socket. Note that in this case, the returned response
-
# object will *not* contain a (meaningful) body.
-
#
-
# +dest+ argument is obsolete.
-
# It still works but you must not use it.
-
#
-
# This method never raises exception.
-
#
-
# response = http.post('/cgi-bin/search.rb', 'query=foo')
-
#
-
# # using block
-
# File.open('result.txt', 'w') {|f|
-
# http.post('/cgi-bin/search.rb', 'query=foo') do |str|
-
# f.write str
-
# end
-
# }
-
#
-
# You should set Content-Type: header field for POST.
-
# If no Content-Type: field given, this method uses
-
# "application/x-www-form-urlencoded" by default.
-
#
-
1
def post(path, data, initheader = nil, dest = nil, &block) # :yield: +body_segment+
-
send_entity(path, data, initheader, dest, Post, &block)
-
end
-
-
# Sends a PATCH request to the +path+ and gets a response,
-
# as an HTTPResponse object.
-
1
def patch(path, data, initheader = nil, dest = nil, &block) # :yield: +body_segment+
-
send_entity(path, data, initheader, dest, Patch, &block)
-
end
-
-
1
def put(path, data, initheader = nil) #:nodoc:
-
request(Put.new(path, initheader), data)
-
end
-
-
# Sends a PROPPATCH request to the +path+ and gets a response,
-
# as an HTTPResponse object.
-
1
def proppatch(path, body, initheader = nil)
-
request(Proppatch.new(path, initheader), body)
-
end
-
-
# Sends a LOCK request to the +path+ and gets a response,
-
# as an HTTPResponse object.
-
1
def lock(path, body, initheader = nil)
-
request(Lock.new(path, initheader), body)
-
end
-
-
# Sends a UNLOCK request to the +path+ and gets a response,
-
# as an HTTPResponse object.
-
1
def unlock(path, body, initheader = nil)
-
request(Unlock.new(path, initheader), body)
-
end
-
-
# Sends a OPTIONS request to the +path+ and gets a response,
-
# as an HTTPResponse object.
-
1
def options(path, initheader = nil)
-
request(Options.new(path, initheader))
-
end
-
-
# Sends a PROPFIND request to the +path+ and gets a response,
-
# as an HTTPResponse object.
-
1
def propfind(path, body = nil, initheader = {'Depth' => '0'})
-
request(Propfind.new(path, initheader), body)
-
end
-
-
# Sends a DELETE request to the +path+ and gets a response,
-
# as an HTTPResponse object.
-
1
def delete(path, initheader = {'Depth' => 'Infinity'})
-
request(Delete.new(path, initheader))
-
end
-
-
# Sends a MOVE request to the +path+ and gets a response,
-
# as an HTTPResponse object.
-
1
def move(path, initheader = nil)
-
request(Move.new(path, initheader))
-
end
-
-
# Sends a COPY request to the +path+ and gets a response,
-
# as an HTTPResponse object.
-
1
def copy(path, initheader = nil)
-
request(Copy.new(path, initheader))
-
end
-
-
# Sends a MKCOL request to the +path+ and gets a response,
-
# as an HTTPResponse object.
-
1
def mkcol(path, body = nil, initheader = nil)
-
request(Mkcol.new(path, initheader), body)
-
end
-
-
# Sends a TRACE request to the +path+ and gets a response,
-
# as an HTTPResponse object.
-
1
def trace(path, initheader = nil)
-
request(Trace.new(path, initheader))
-
end
-
-
# Sends a GET request to the +path+.
-
# Returns the response as a Net::HTTPResponse object.
-
#
-
# When called with a block, passes an HTTPResponse object to the block.
-
# The body of the response will not have been read yet;
-
# the block can process it using HTTPResponse#read_body,
-
# if desired.
-
#
-
# Returns the response.
-
#
-
# This method never raises Net::* exceptions.
-
#
-
# response = http.request_get('/index.html')
-
# # The entity body is already read in this case.
-
# p response['content-type']
-
# puts response.body
-
#
-
# # Using a block
-
# http.request_get('/index.html') {|response|
-
# p response['content-type']
-
# response.read_body do |str| # read body now
-
# print str
-
# end
-
# }
-
#
-
1
def request_get(path, initheader = nil, &block) # :yield: +response+
-
request(Get.new(path, initheader), &block)
-
end
-
-
# Sends a HEAD request to the +path+ and returns the response
-
# as a Net::HTTPResponse object.
-
#
-
# Returns the response.
-
#
-
# This method never raises Net::* exceptions.
-
#
-
# response = http.request_head('/index.html')
-
# p response['content-type']
-
#
-
1
def request_head(path, initheader = nil, &block)
-
request(Head.new(path, initheader), &block)
-
end
-
-
# Sends a POST request to the +path+.
-
#
-
# Returns the response as a Net::HTTPResponse object.
-
#
-
# When called with a block, the block is passed an HTTPResponse
-
# object. The body of that response will not have been read yet;
-
# the block can process it using HTTPResponse#read_body, if desired.
-
#
-
# Returns the response.
-
#
-
# This method never raises Net::* exceptions.
-
#
-
# # example
-
# response = http.request_post('/cgi-bin/nice.rb', 'datadatadata...')
-
# p response.status
-
# puts response.body # body is already read in this case
-
#
-
# # using block
-
# http.request_post('/cgi-bin/nice.rb', 'datadatadata...') {|response|
-
# p response.status
-
# p response['content-type']
-
# response.read_body do |str| # read body now
-
# print str
-
# end
-
# }
-
#
-
1
def request_post(path, data, initheader = nil, &block) # :yield: +response+
-
request Post.new(path, initheader), data, &block
-
end
-
-
1
def request_put(path, data, initheader = nil, &block) #:nodoc:
-
request Put.new(path, initheader), data, &block
-
end
-
-
1
alias get2 request_get #:nodoc: obsolete
-
1
alias head2 request_head #:nodoc: obsolete
-
1
alias post2 request_post #:nodoc: obsolete
-
1
alias put2 request_put #:nodoc: obsolete
-
-
-
# Sends an HTTP request to the HTTP server.
-
# Also sends a DATA string if +data+ is given.
-
#
-
# Returns a Net::HTTPResponse object.
-
#
-
# This method never raises Net::* exceptions.
-
#
-
# response = http.send_request('GET', '/index.html')
-
# puts response.body
-
#
-
1
def send_request(name, path, data = nil, header = nil)
-
has_response_body = name != 'HEAD'
-
r = HTTPGenericRequest.new(name,(data ? true : false),has_response_body,path,header)
-
request r, data
-
end
-
-
# Sends an HTTPRequest object +req+ to the HTTP server.
-
#
-
# If +req+ is a Net::HTTP::Post or Net::HTTP::Put request containing
-
# data, the data is also sent. Providing data for a Net::HTTP::Head or
-
# Net::HTTP::Get request results in an ArgumentError.
-
#
-
# Returns an HTTPResponse object.
-
#
-
# When called with a block, passes an HTTPResponse object to the block.
-
# The body of the response will not have been read yet;
-
# the block can process it using HTTPResponse#read_body,
-
# if desired.
-
#
-
# This method never raises Net::* exceptions.
-
#
-
1
def request(req, body = nil, &block) # :yield: +response+
-
3
unless started?
-
start {
-
req['connection'] ||= 'close'
-
return request(req, body, &block)
-
}
-
end
-
3
if proxy_user()
-
req.proxy_basic_auth proxy_user(), proxy_pass() unless use_ssl?
-
end
-
3
req.set_body_internal body
-
3
res = transport_request(req, &block)
-
3
if sspi_auth?(res)
-
sspi_auth(req)
-
res = transport_request(req, &block)
-
end
-
3
res
-
end
-
-
1
private
-
-
# Executes a request which uses a representation
-
# and returns its body.
-
1
def send_entity(path, data, initheader, dest, type, &block)
-
res = nil
-
request(type.new(path, initheader), data) {|r|
-
r.read_body dest, &block
-
res = r
-
}
-
res
-
end
-
-
1
IDEMPOTENT_METHODS_ = %w/GET HEAD PUT DELETE OPTIONS TRACE/ # :nodoc:
-
-
1
def transport_request(req)
-
3
count = 0
-
begin
-
3
begin_transport req
-
3
res = catch(:response) {
-
begin
-
3
req.exec @socket, @curr_http_version, edit_path(req.path)
-
rescue Errno::EPIPE
-
# Failure when writing full request, but we can probably
-
# still read the received response.
-
end
-
-
3
begin
-
3
res = HTTPResponse.read_new(@socket)
-
3
res.decode_content = req.decode_content
-
end while res.kind_of?(HTTPInformation)
-
-
3
res.uri = req.uri
-
-
3
res
-
}
-
3
res.reading_body(@socket, req.response_body_permitted?) {
-
3
yield res if block_given?
-
}
-
rescue Net::OpenTimeout
-
raise
-
rescue Net::ReadTimeout, IOError, EOFError,
-
Errno::ECONNRESET, Errno::ECONNABORTED, Errno::EPIPE, Errno::ETIMEDOUT,
-
# avoid a dependency on OpenSSL
-
defined?(OpenSSL::SSL) ? OpenSSL::SSL::SSLError : IOError,
-
Timeout::Error => exception
-
if count < max_retries && IDEMPOTENT_METHODS_.include?(req.method)
-
count += 1
-
@socket.close if @socket
-
D "Conn close because of error #{exception}, and retry"
-
retry
-
end
-
D "Conn close because of error #{exception}"
-
@socket.close if @socket
-
raise
-
end
-
-
3
end_transport req, res
-
3
res
-
rescue => exception
-
D "Conn close because of error #{exception}"
-
@socket.close if @socket
-
raise exception
-
end
-
-
1
def begin_transport(req)
-
3
if @socket.closed?
-
connect
-
3
elsif @last_communicated
-
if @last_communicated + @keep_alive_timeout < Process.clock_gettime(Process::CLOCK_MONOTONIC)
-
D 'Conn close because of keep_alive_timeout'
-
@socket.close
-
connect
-
elsif @socket.io.to_io.wait_readable(0) && @socket.eof?
-
D "Conn close because of EOF"
-
@socket.close
-
connect
-
end
-
end
-
-
3
if not req.response_body_permitted? and @close_on_empty_response
-
req['connection'] ||= 'close'
-
end
-
-
3
req.update_uri address, port, use_ssl?
-
3
req['host'] ||= addr_port()
-
end
-
-
1
def end_transport(req, res)
-
3
@curr_http_version = res.http_version
-
3
@last_communicated = nil
-
3
if @socket.closed?
-
D 'Conn socket closed'
-
3
elsif not res.body and @close_on_empty_response
-
D 'Conn close'
-
@socket.close
-
3
elsif keep_alive?(req, res)
-
3
D 'Conn keep-alive'
-
3
@last_communicated = Process.clock_gettime(Process::CLOCK_MONOTONIC)
-
else
-
D 'Conn close'
-
@socket.close
-
end
-
end
-
-
1
def keep_alive?(req, res)
-
3
return false if req.connection_close?
-
3
if @curr_http_version <= '1.0'
-
res.connection_keep_alive?
-
else # HTTP/1.1 or later
-
3
not res.connection_close?
-
end
-
end
-
-
1
def sspi_auth?(res)
-
3
return false unless @sspi_enabled
-
if res.kind_of?(HTTPProxyAuthenticationRequired) and
-
proxy? and res["Proxy-Authenticate"].include?("Negotiate")
-
begin
-
require 'win32/sspi'
-
true
-
rescue LoadError
-
false
-
end
-
else
-
false
-
end
-
end
-
-
1
def sspi_auth(req)
-
n = Win32::SSPI::NegotiateAuth.new
-
req["Proxy-Authorization"] = "Negotiate #{n.get_initial_token}"
-
# Some versions of ISA will close the connection if this isn't present.
-
req["Connection"] = "Keep-Alive"
-
req["Proxy-Connection"] = "Keep-Alive"
-
res = transport_request(req)
-
authphrase = res["Proxy-Authenticate"] or return res
-
req["Proxy-Authorization"] = "Negotiate #{n.complete_authentication(authphrase)}"
-
rescue => err
-
raise HTTPAuthenticationError.new('HTTP authentication failed', err)
-
end
-
-
#
-
# utils
-
#
-
-
1
private
-
-
1
def addr_port
-
addr = address
-
addr = "[#{addr}]" if addr.include?(":")
-
default_port = use_ssl? ? HTTP.https_default_port : HTTP.http_default_port
-
default_port == port ? addr : "#{addr}:#{port}"
-
end
-
-
1
def D(msg)
-
9
return unless @debug_output
-
@debug_output << msg
-
@debug_output << "\n"
-
end
-
end
-
-
end
-
-
1
require_relative 'http/exceptions'
-
-
1
require_relative 'http/header'
-
-
1
require_relative 'http/generic_request'
-
1
require_relative 'http/request'
-
1
require_relative 'http/requests'
-
-
1
require_relative 'http/response'
-
1
require_relative 'http/responses'
-
-
1
require_relative 'http/proxy_delta'
-
-
1
require_relative 'http/backward'
-
# frozen_string_literal: false
-
# for backward compatibility
-
-
# :enddoc:
-
-
1
class Net::HTTP
-
1
ProxyMod = ProxyDelta
-
end
-
-
1
module Net
-
1
HTTPSession = Net::HTTP
-
end
-
-
1
module Net::NetPrivate
-
1
HTTPRequest = ::Net::HTTPRequest
-
end
-
-
1
Net::HTTPInformationCode = Net::HTTPInformation
-
1
Net::HTTPSuccessCode = Net::HTTPSuccess
-
1
Net::HTTPRedirectionCode = Net::HTTPRedirection
-
1
Net::HTTPRetriableCode = Net::HTTPRedirection
-
1
Net::HTTPClientErrorCode = Net::HTTPClientError
-
1
Net::HTTPFatalErrorCode = Net::HTTPClientError
-
1
Net::HTTPServerErrorCode = Net::HTTPServerError
-
1
Net::HTTPResponceReceiver = Net::HTTPResponse
-
-
# frozen_string_literal: false
-
# Net::HTTP exception class.
-
# You cannot use Net::HTTPExceptions directly; instead, you must use
-
# its subclasses.
-
1
module Net::HTTPExceptions
-
1
def initialize(msg, res) #:nodoc:
-
super msg
-
@response = res
-
end
-
1
attr_reader :response
-
1
alias data response #:nodoc: obsolete
-
end
-
1
class Net::HTTPError < Net::ProtocolError
-
1
include Net::HTTPExceptions
-
end
-
1
class Net::HTTPRetriableError < Net::ProtoRetriableError
-
1
include Net::HTTPExceptions
-
end
-
1
class Net::HTTPServerException < Net::ProtoServerError
-
# We cannot use the name "HTTPServerError", it is the name of the response.
-
1
include Net::HTTPExceptions
-
end
-
-
# for compatibility
-
1
Net::HTTPClientException = Net::HTTPServerException
-
-
1
class Net::HTTPFatalError < Net::ProtoFatalError
-
1
include Net::HTTPExceptions
-
end
-
-
1
module Net
-
1
deprecate_constant(:HTTPServerException)
-
end
-
# frozen_string_literal: false
-
# HTTPGenericRequest is the parent of the Net::HTTPRequest class.
-
# Do not use this directly; use a subclass of Net::HTTPRequest.
-
#
-
# Mixes in the Net::HTTPHeader module to provide easier access to HTTP headers.
-
#
-
1
class Net::HTTPGenericRequest
-
-
1
include Net::HTTPHeader
-
-
1
def initialize(m, reqbody, resbody, uri_or_path, initheader = nil)
-
3
@method = m
-
3
@request_has_body = reqbody
-
3
@response_has_body = resbody
-
-
3
if URI === uri_or_path then
-
3
raise ArgumentError, "not an HTTP URI" unless URI::HTTP === uri_or_path
-
3
raise ArgumentError, "no host component for URI" unless uri_or_path.hostname
-
3
@uri = uri_or_path.dup
-
3
host = @uri.hostname.dup
-
3
host << ":".freeze << @uri.port.to_s if @uri.port != @uri.default_port
-
3
@path = uri_or_path.request_uri
-
3
raise ArgumentError, "no HTTP request path given" unless @path
-
else
-
@uri = nil
-
host = nil
-
raise ArgumentError, "no HTTP request path given" unless uri_or_path
-
raise ArgumentError, "HTTP request path is empty" if uri_or_path.empty?
-
@path = uri_or_path.dup
-
end
-
-
3
@decode_content = false
-
-
3
if @response_has_body and Net::HTTP::HAVE_ZLIB then
-
3
if !initheader ||
-
!initheader.keys.any? { |k|
-
%w[accept-encoding range].include? k.downcase
-
} then
-
3
@decode_content = true
-
3
initheader = initheader ? initheader.dup : {}
-
3
initheader["accept-encoding"] =
-
"gzip;q=1.0,deflate;q=0.6,identity;q=0.3"
-
end
-
end
-
-
3
initialize_http_header initheader
-
3
self['Accept'] ||= '*/*'
-
3
self['User-Agent'] ||= 'Ruby'
-
3
self['Host'] ||= host if host
-
3
@body = nil
-
3
@body_stream = nil
-
3
@body_data = nil
-
end
-
-
1
attr_reader :method
-
1
attr_reader :path
-
1
attr_reader :uri
-
-
# Automatically set to false if the user sets the Accept-Encoding header.
-
# This indicates they wish to handle Content-encoding in responses
-
# themselves.
-
1
attr_reader :decode_content
-
-
1
def inspect
-
"\#<#{self.class} #{@method}>"
-
end
-
-
##
-
# Don't automatically decode response content-encoding if the user indicates
-
# they want to handle it.
-
-
1
def []=(key, val) # :nodoc:
-
9
@decode_content = false if key.downcase == 'accept-encoding'
-
-
9
super key, val
-
end
-
-
1
def request_body_permitted?
-
@request_has_body
-
end
-
-
1
def response_body_permitted?
-
6
@response_has_body
-
end
-
-
1
def body_exist?
-
warn "Net::HTTPRequest#body_exist? is obsolete; use response_body_permitted?", uplevel: 1 if $VERBOSE
-
response_body_permitted?
-
end
-
-
1
attr_reader :body
-
-
1
def body=(str)
-
3
@body = str
-
3
@body_stream = nil
-
3
@body_data = nil
-
3
str
-
end
-
-
1
attr_reader :body_stream
-
-
1
def body_stream=(input)
-
@body = nil
-
@body_stream = input
-
@body_data = nil
-
input
-
end
-
-
1
def set_body_internal(str) #:nodoc: internal use only
-
3
raise ArgumentError, "both of body argument and HTTPRequest#body set" if str and (@body or @body_stream)
-
3
self.body = str if str
-
3
if @body.nil? && @body_stream.nil? && @body_data.nil? && request_body_permitted?
-
self.body = ''
-
end
-
end
-
-
#
-
# write
-
#
-
-
1
def exec(sock, ver, path) #:nodoc: internal use only
-
3
if @body
-
3
send_request_with_body sock, ver, path, @body
-
elsif @body_stream
-
send_request_with_body_stream sock, ver, path, @body_stream
-
elsif @body_data
-
send_request_with_body_data sock, ver, path, @body_data
-
else
-
write_header sock, ver, path
-
end
-
end
-
-
1
def update_uri(addr, port, ssl) # :nodoc: internal use only
-
# reflect the connection and @path to @uri
-
3
return unless @uri
-
-
3
if ssl
-
scheme = 'https'.freeze
-
klass = URI::HTTPS
-
else
-
3
scheme = 'http'.freeze
-
3
klass = URI::HTTP
-
end
-
-
3
if host = self['host']
-
3
host.sub!(/:.*/s, ''.freeze)
-
elsif host = @uri.host
-
else
-
host = addr
-
end
-
# convert the class of the URI
-
3
if @uri.is_a?(klass)
-
3
@uri.host = host
-
3
@uri.port = port
-
else
-
@uri = klass.new(
-
scheme, @uri.userinfo,
-
host, port, nil,
-
@uri.path, nil, @uri.query, nil)
-
end
-
end
-
-
1
private
-
-
1
class Chunker #:nodoc:
-
1
def initialize(sock)
-
@sock = sock
-
@prev = nil
-
end
-
-
1
def write(buf)
-
# avoid memcpy() of buf, buf can huge and eat memory bandwidth
-
rv = buf.bytesize
-
@sock.write("#{rv.to_s(16)}\r\n", buf, "\r\n")
-
rv
-
end
-
-
1
def finish
-
@sock.write("0\r\n\r\n")
-
end
-
end
-
-
1
def send_request_with_body(sock, ver, path, body)
-
3
self.content_length = body.bytesize
-
3
delete 'Transfer-Encoding'
-
3
supply_default_content_type
-
3
write_header sock, ver, path
-
3
wait_for_continue sock, ver if sock.continue_timeout
-
3
sock.write body
-
end
-
-
1
def send_request_with_body_stream(sock, ver, path, f)
-
unless content_length() or chunked?
-
raise ArgumentError,
-
"Content-Length not given and Transfer-Encoding is not `chunked'"
-
end
-
supply_default_content_type
-
write_header sock, ver, path
-
wait_for_continue sock, ver if sock.continue_timeout
-
if chunked?
-
chunker = Chunker.new(sock)
-
IO.copy_stream(f, chunker)
-
chunker.finish
-
else
-
# copy_stream can sendfile() to sock.io unless we use SSL.
-
# If sock.io is an SSLSocket, copy_stream will hit SSL_write()
-
IO.copy_stream(f, sock.io)
-
end
-
end
-
-
1
def send_request_with_body_data(sock, ver, path, params)
-
if /\Amultipart\/form-data\z/i !~ self.content_type
-
self.content_type = 'application/x-www-form-urlencoded'
-
return send_request_with_body(sock, ver, path, URI.encode_www_form(params))
-
end
-
-
opt = @form_option.dup
-
require 'securerandom' unless defined?(SecureRandom)
-
opt[:boundary] ||= SecureRandom.urlsafe_base64(40)
-
self.set_content_type(self.content_type, boundary: opt[:boundary])
-
if chunked?
-
write_header sock, ver, path
-
encode_multipart_form_data(sock, params, opt)
-
else
-
require 'tempfile'
-
file = Tempfile.new('multipart')
-
file.binmode
-
encode_multipart_form_data(file, params, opt)
-
file.rewind
-
self.content_length = file.size
-
write_header sock, ver, path
-
IO.copy_stream(file, sock)
-
file.close(true)
-
end
-
end
-
-
1
def encode_multipart_form_data(out, params, opt)
-
charset = opt[:charset]
-
boundary = opt[:boundary]
-
require 'securerandom' unless defined?(SecureRandom)
-
boundary ||= SecureRandom.urlsafe_base64(40)
-
chunked_p = chunked?
-
-
buf = ''
-
params.each do |key, value, h={}|
-
key = quote_string(key, charset)
-
filename =
-
h.key?(:filename) ? h[:filename] :
-
value.respond_to?(:to_path) ? File.basename(value.to_path) :
-
nil
-
-
buf << "--#{boundary}\r\n"
-
if filename
-
filename = quote_string(filename, charset)
-
type = h[:content_type] || 'application/octet-stream'
-
buf << "Content-Disposition: form-data; " \
-
"name=\"#{key}\"; filename=\"#{filename}\"\r\n" \
-
"Content-Type: #{type}\r\n\r\n"
-
if !out.respond_to?(:write) || !value.respond_to?(:read)
-
# if +out+ is not an IO or +value+ is not an IO
-
buf << (value.respond_to?(:read) ? value.read : value)
-
elsif value.respond_to?(:size) && chunked_p
-
# if +out+ is an IO and +value+ is a File, use IO.copy_stream
-
flush_buffer(out, buf, chunked_p)
-
out << "%x\r\n" % value.size if chunked_p
-
IO.copy_stream(value, out)
-
out << "\r\n" if chunked_p
-
else
-
# +out+ is an IO, and +value+ is not a File but an IO
-
flush_buffer(out, buf, chunked_p)
-
1 while flush_buffer(out, value.read(4096), chunked_p)
-
end
-
else
-
# non-file field:
-
# HTML5 says, "The parts of the generated multipart/form-data
-
# resource that correspond to non-file fields must not have a
-
# Content-Type header specified."
-
buf << "Content-Disposition: form-data; name=\"#{key}\"\r\n\r\n"
-
buf << (value.respond_to?(:read) ? value.read : value)
-
end
-
buf << "\r\n"
-
end
-
buf << "--#{boundary}--\r\n"
-
flush_buffer(out, buf, chunked_p)
-
out << "0\r\n\r\n" if chunked_p
-
end
-
-
1
def quote_string(str, charset)
-
str = str.encode(charset, fallback:->(c){'&#%d;'%c.encode("UTF-8").ord}) if charset
-
str.gsub(/[\\"]/, '\\\\\&')
-
end
-
-
1
def flush_buffer(out, buf, chunked_p)
-
return unless buf
-
out << "%x\r\n"%buf.bytesize if chunked_p
-
out << buf
-
out << "\r\n" if chunked_p
-
buf.clear
-
end
-
-
1
def supply_default_content_type
-
3
return if content_type()
-
warn 'net/http: Content-Type did not set; using application/x-www-form-urlencoded', uplevel: 1 if $VERBOSE
-
set_content_type 'application/x-www-form-urlencoded'
-
end
-
-
##
-
# Waits up to the continue timeout for a response from the server provided
-
# we're speaking HTTP 1.1 and are expecting a 100-continue response.
-
-
1
def wait_for_continue(sock, ver)
-
if ver >= '1.1' and @header['expect'] and
-
@header['expect'].include?('100-continue')
-
if sock.io.to_io.wait_readable(sock.continue_timeout)
-
res = Net::HTTPResponse.read_new(sock)
-
unless res.kind_of?(Net::HTTPContinue)
-
res.decode_content = @decode_content
-
throw :response, res
-
end
-
end
-
end
-
end
-
-
1
def write_header(sock, ver, path)
-
3
reqline = "#{@method} #{path} HTTP/#{ver}"
-
3
if /[\r\n]/ =~ reqline
-
raise ArgumentError, "A Request-Line must not contain CR or LF"
-
end
-
3
buf = ""
-
3
buf << reqline << "\r\n"
-
3
each_capitalized do |k,v|
-
18
buf << "#{k}: #{v}\r\n"
-
end
-
3
buf << "\r\n"
-
3
sock.write buf
-
end
-
-
end
-
-
# frozen_string_literal: false
-
# The HTTPHeader module defines methods for reading and writing
-
# HTTP headers.
-
#
-
# It is used as a mixin by other classes, to provide hash-like
-
# access to HTTP header values. Unlike raw hash access, HTTPHeader
-
# provides access via case-insensitive keys. It also provides
-
# methods for accessing commonly-used HTTP header values in more
-
# convenient formats.
-
#
-
1
module Net::HTTPHeader
-
-
1
def initialize_http_header(initheader)
-
6
@header = {}
-
6
return unless initheader
-
3
initheader.each do |key, value|
-
3
warn "net/http: duplicated HTTP header: #{key}", uplevel: 3 if key?(key) and $VERBOSE
-
3
if value.nil?
-
warn "net/http: nil HTTP header: #{key}", uplevel: 3 if $VERBOSE
-
else
-
3
value = value.strip # raise error for invalid byte sequences
-
3
if value.count("\r\n") > 0
-
raise ArgumentError, "header #{key} has field value #{value.inspect}, this cannot include CR/LF"
-
end
-
3
@header[key.downcase.to_s] = [value]
-
end
-
end
-
end
-
-
1
def size #:nodoc: obsolete
-
@header.size
-
end
-
-
1
alias length size #:nodoc: obsolete
-
-
# Returns the header field corresponding to the case-insensitive key.
-
# For example, a key of "Content-Type" might return "text/html"
-
1
def [](key)
-
36
a = @header[key.downcase.to_s] or return nil
-
21
a.join(', ')
-
end
-
-
# Sets the header field corresponding to the case-insensitive key.
-
1
def []=(key, val)
-
9
unless val
-
@header.delete key.downcase.to_s
-
return val
-
end
-
9
set_field(key, val)
-
end
-
-
# [Ruby 1.8.3]
-
# Adds a value to a named header field, instead of replacing its value.
-
# Second argument +val+ must be a String.
-
# See also #[]=, #[] and #get_fields.
-
#
-
# request.add_field 'X-My-Header', 'a'
-
# p request['X-My-Header'] #=> "a"
-
# p request.get_fields('X-My-Header') #=> ["a"]
-
# request.add_field 'X-My-Header', 'b'
-
# p request['X-My-Header'] #=> "a, b"
-
# p request.get_fields('X-My-Header') #=> ["a", "b"]
-
# request.add_field 'X-My-Header', 'c'
-
# p request['X-My-Header'] #=> "a, b, c"
-
# p request.get_fields('X-My-Header') #=> ["a", "b", "c"]
-
#
-
1
def add_field(key, val)
-
6
stringified_downcased_key = key.downcase.to_s
-
6
if @header.key?(stringified_downcased_key)
-
append_field_value(@header[stringified_downcased_key], val)
-
else
-
6
set_field(key, val)
-
end
-
end
-
-
1
private def set_field(key, val)
-
15
case val
-
when Enumerable
-
ary = []
-
append_field_value(ary, val)
-
@header[key.downcase.to_s] = ary
-
else
-
15
val = val.to_s # for compatibility use to_s instead of to_str
-
15
if val.b.count("\r\n") > 0
-
raise ArgumentError, 'header field value cannot include CR/LF'
-
end
-
15
@header[key.downcase.to_s] = [val]
-
end
-
end
-
-
1
private def append_field_value(ary, val)
-
case val
-
when Enumerable
-
val.each{|x| append_field_value(ary, x)}
-
else
-
val = val.to_s
-
if /[\r\n]/n.match?(val.b)
-
raise ArgumentError, 'header field value cannot include CR/LF'
-
end
-
ary.push val
-
end
-
end
-
-
# [Ruby 1.8.3]
-
# Returns an array of header field strings corresponding to the
-
# case-insensitive +key+. This method allows you to get duplicated
-
# header fields without any processing. See also #[].
-
#
-
# p response.get_fields('Set-Cookie')
-
# #=> ["session=al98axx; expires=Fri, 31-Dec-1999 23:58:23",
-
# "query=rubyscript; expires=Fri, 31-Dec-1999 23:58:23"]
-
# p response['Set-Cookie']
-
# #=> "session=al98axx; expires=Fri, 31-Dec-1999 23:58:23, query=rubyscript; expires=Fri, 31-Dec-1999 23:58:23"
-
#
-
1
def get_fields(key)
-
stringified_downcased_key = key.downcase.to_s
-
return nil unless @header[stringified_downcased_key]
-
@header[stringified_downcased_key].dup
-
end
-
-
# Returns the header field corresponding to the case-insensitive key.
-
# Returns the default value +args+, or the result of the block, or
-
# raises an IndexError if there's no header field named +key+
-
# See Hash#fetch
-
1
def fetch(key, *args, &block) #:yield: +key+
-
a = @header.fetch(key.downcase.to_s, *args, &block)
-
a.kind_of?(Array) ? a.join(', ') : a
-
end
-
-
# Iterates through the header names and values, passing in the name
-
# and value to the code block supplied.
-
#
-
# Returns an enumerator if no block is given.
-
#
-
# Example:
-
#
-
# response.header.each_header {|key,value| puts "#{key} = #{value}" }
-
#
-
1
def each_header #:yield: +key+, +value+
-
block_given? or return enum_for(__method__) { @header.size }
-
@header.each do |k,va|
-
yield k, va.join(', ')
-
end
-
end
-
-
1
alias each each_header
-
-
# Iterates through the header names in the header, passing
-
# each header name to the code block.
-
#
-
# Returns an enumerator if no block is given.
-
1
def each_name(&block) #:yield: +key+
-
block_given? or return enum_for(__method__) { @header.size }
-
@header.each_key(&block)
-
end
-
-
1
alias each_key each_name
-
-
# Iterates through the header names in the header, passing
-
# capitalized header names to the code block.
-
#
-
# Note that header names are capitalized systematically;
-
# capitalization may not match that used by the remote HTTP
-
# server in its response.
-
#
-
# Returns an enumerator if no block is given.
-
1
def each_capitalized_name #:yield: +key+
-
block_given? or return enum_for(__method__) { @header.size }
-
@header.each_key do |k|
-
yield capitalize(k)
-
end
-
end
-
-
# Iterates through header values, passing each value to the
-
# code block.
-
#
-
# Returns an enumerator if no block is given.
-
1
def each_value #:yield: +value+
-
block_given? or return enum_for(__method__) { @header.size }
-
@header.each_value do |va|
-
yield va.join(', ')
-
end
-
end
-
-
# Removes a header field, specified by case-insensitive key.
-
1
def delete(key)
-
3
@header.delete(key.downcase.to_s)
-
end
-
-
# true if +key+ header exists.
-
1
def key?(key)
-
6
@header.key?(key.downcase.to_s)
-
end
-
-
# Returns a Hash consisting of header names and array of values.
-
# e.g.
-
# {"cache-control" => ["private"],
-
# "content-type" => ["text/html"],
-
# "date" => ["Wed, 22 Jun 2005 22:11:50 GMT"]}
-
1
def to_hash
-
@header.dup
-
end
-
-
# As for #each_header, except the keys are provided in capitalized form.
-
#
-
# Note that header names are capitalized systematically;
-
# capitalization may not match that used by the remote HTTP
-
# server in its response.
-
#
-
# Returns an enumerator if no block is given.
-
1
def each_capitalized
-
3
block_given? or return enum_for(__method__) { @header.size }
-
3
@header.each do |k,v|
-
18
yield capitalize(k), v.join(', ')
-
end
-
end
-
-
1
alias canonical_each each_capitalized
-
-
1
def capitalize(name)
-
48
name.to_s.split(/-/).map {|s| s.capitalize }.join('-')
-
end
-
1
private :capitalize
-
-
# Returns an Array of Range objects which represent the Range:
-
# HTTP header field, or +nil+ if there is no such header.
-
1
def range
-
return nil unless @header['range']
-
-
value = self['Range']
-
# byte-range-set = *( "," OWS ) ( byte-range-spec / suffix-byte-range-spec )
-
# *( OWS "," [ OWS ( byte-range-spec / suffix-byte-range-spec ) ] )
-
# corrected collected ABNF
-
# http://tools.ietf.org/html/draft-ietf-httpbis-p5-range-19#section-5.4.1
-
# http://tools.ietf.org/html/draft-ietf-httpbis-p5-range-19#appendix-C
-
# http://tools.ietf.org/html/draft-ietf-httpbis-p1-messaging-19#section-3.2.5
-
unless /\Abytes=((?:,[ \t]*)*(?:\d+-\d*|-\d+)(?:[ \t]*,(?:[ \t]*\d+-\d*|-\d+)?)*)\z/ =~ value
-
raise Net::HTTPHeaderSyntaxError, "invalid syntax for byte-ranges-specifier: '#{value}'"
-
end
-
-
byte_range_set = $1
-
result = byte_range_set.split(/,/).map {|spec|
-
m = /(\d+)?\s*-\s*(\d+)?/i.match(spec) or
-
raise Net::HTTPHeaderSyntaxError, "invalid byte-range-spec: '#{spec}'"
-
d1 = m[1].to_i
-
d2 = m[2].to_i
-
if m[1] and m[2]
-
if d1 > d2
-
raise Net::HTTPHeaderSyntaxError, "last-byte-pos MUST greater than or equal to first-byte-pos but '#{spec}'"
-
end
-
d1..d2
-
elsif m[1]
-
d1..-1
-
elsif m[2]
-
-d2..-1
-
else
-
raise Net::HTTPHeaderSyntaxError, 'range is not specified'
-
end
-
}
-
# if result.empty?
-
# byte-range-set must include at least one byte-range-spec or suffix-byte-range-spec
-
# but above regexp already denies it.
-
if result.size == 1 && result[0].begin == 0 && result[0].end == -1
-
raise Net::HTTPHeaderSyntaxError, 'only one suffix-byte-range-spec with zero suffix-length'
-
end
-
result
-
end
-
-
# Sets the HTTP Range: header.
-
# Accepts either a Range object as a single argument,
-
# or a beginning index and a length from that index.
-
# Example:
-
#
-
# req.range = (0..1023)
-
# req.set_range 0, 1023
-
#
-
1
def set_range(r, e = nil)
-
unless r
-
@header.delete 'range'
-
return r
-
end
-
r = (r...r+e) if e
-
case r
-
when Numeric
-
n = r.to_i
-
rangestr = (n > 0 ? "0-#{n-1}" : "-#{-n}")
-
when Range
-
first = r.first
-
last = r.end
-
last -= 1 if r.exclude_end?
-
if last == -1
-
rangestr = (first > 0 ? "#{first}-" : "-#{-first}")
-
else
-
raise Net::HTTPHeaderSyntaxError, 'range.first is negative' if first < 0
-
raise Net::HTTPHeaderSyntaxError, 'range.last is negative' if last < 0
-
raise Net::HTTPHeaderSyntaxError, 'must be .first < .last' if first > last
-
rangestr = "#{first}-#{last}"
-
end
-
else
-
raise TypeError, 'Range/Integer is required'
-
end
-
@header['range'] = ["bytes=#{rangestr}"]
-
r
-
end
-
-
1
alias range= set_range
-
-
# Returns an Integer object which represents the HTTP Content-Length:
-
# header field, or +nil+ if that field was not provided.
-
1
def content_length
-
3
return nil unless key?('Content-Length')
-
3
len = self['Content-Length'].slice(/\d+/) or
-
raise Net::HTTPHeaderSyntaxError, 'wrong Content-Length format'
-
3
len.to_i
-
end
-
-
1
def content_length=(len)
-
3
unless len
-
@header.delete 'content-length'
-
return nil
-
end
-
3
@header['content-length'] = [len.to_i.to_s]
-
end
-
-
# Returns "true" if the "transfer-encoding" header is present and
-
# set to "chunked". This is an HTTP/1.1 feature, allowing
-
# the content to be sent in "chunks" without at the outset
-
# stating the entire content length.
-
1
def chunked?
-
3
return false unless @header['transfer-encoding']
-
field = self['Transfer-Encoding']
-
(/(?:\A|[^\-\w])chunked(?![\-\w])/i =~ field) ? true : false
-
end
-
-
# Returns a Range object which represents the value of the Content-Range:
-
# header field.
-
# For a partial entity body, this indicates where this fragment
-
# fits inside the full entity body, as range of byte offsets.
-
1
def content_range
-
return nil unless @header['content-range']
-
m = %r<bytes\s+(\d+)-(\d+)/(\d+|\*)>i.match(self['Content-Range']) or
-
raise Net::HTTPHeaderSyntaxError, 'wrong Content-Range format'
-
m[1].to_i .. m[2].to_i
-
end
-
-
# The length of the range represented in Content-Range: header.
-
1
def range_length
-
r = content_range() or return nil
-
r.end - r.begin + 1
-
end
-
-
# Returns a content type string such as "text/html".
-
# This method returns nil if Content-Type: header field does not exist.
-
1
def content_type
-
3
return nil unless main_type()
-
3
if sub_type()
-
3
then "#{main_type()}/#{sub_type()}"
-
else main_type()
-
end
-
end
-
-
# Returns a content type string such as "text".
-
# This method returns nil if Content-Type: header field does not exist.
-
1
def main_type
-
6
return nil unless @header['content-type']
-
6
self['Content-Type'].split(';').first.to_s.split('/')[0].to_s.strip
-
end
-
-
# Returns a content type string such as "html".
-
# This method returns nil if Content-Type: header field does not exist
-
# or sub-type is not given (e.g. "Content-Type: text").
-
1
def sub_type
-
6
return nil unless @header['content-type']
-
6
_, sub = *self['Content-Type'].split(';').first.to_s.split('/')
-
6
return nil unless sub
-
6
sub.strip
-
end
-
-
# Any parameters specified for the content type, returned as a Hash.
-
# For example, a header of Content-Type: text/html; charset=EUC-JP
-
# would result in type_params returning {'charset' => 'EUC-JP'}
-
1
def type_params
-
result = {}
-
list = self['Content-Type'].to_s.split(';')
-
list.shift
-
list.each do |param|
-
k, v = *param.split('=', 2)
-
result[k.strip] = v.strip
-
end
-
result
-
end
-
-
# Sets the content type in an HTTP header.
-
# The +type+ should be a full HTTP content type, e.g. "text/html".
-
# The +params+ are an optional Hash of parameters to add after the
-
# content type, e.g. {'charset' => 'iso-8859-1'}
-
1
def set_content_type(type, params = {})
-
3
@header['content-type'] = [type + params.map{|k,v|"; #{k}=#{v}"}.join('')]
-
end
-
-
1
alias content_type= set_content_type
-
-
# Set header fields and a body from HTML form data.
-
# +params+ should be an Array of Arrays or
-
# a Hash containing HTML form data.
-
# Optional argument +sep+ means data record separator.
-
#
-
# Values are URL encoded as necessary and the content-type is set to
-
# application/x-www-form-urlencoded
-
#
-
# Example:
-
# http.form_data = {"q" => "ruby", "lang" => "en"}
-
# http.form_data = {"q" => ["ruby", "perl"], "lang" => "en"}
-
# http.set_form_data({"q" => "ruby", "lang" => "en"}, ';')
-
#
-
1
def set_form_data(params, sep = '&')
-
query = URI.encode_www_form(params)
-
query.gsub!(/&/, sep) if sep != '&'
-
self.body = query
-
self.content_type = 'application/x-www-form-urlencoded'
-
end
-
-
1
alias form_data= set_form_data
-
-
# Set an HTML form data set.
-
# +params+ is the form data set; it is an Array of Arrays or a Hash
-
# +enctype is the type to encode the form data set.
-
# It is application/x-www-form-urlencoded or multipart/form-data.
-
# +formopt+ is an optional hash to specify the detail.
-
#
-
# boundary:: the boundary of the multipart message
-
# charset:: the charset of the message. All names and the values of
-
# non-file fields are encoded as the charset.
-
#
-
# Each item of params is an array and contains following items:
-
# +name+:: the name of the field
-
# +value+:: the value of the field, it should be a String or a File
-
# +opt+:: an optional hash to specify additional information
-
#
-
# Each item is a file field or a normal field.
-
# If +value+ is a File object or the +opt+ have a filename key,
-
# the item is treated as a file field.
-
#
-
# If Transfer-Encoding is set as chunked, this send the request in
-
# chunked encoding. Because chunked encoding is HTTP/1.1 feature,
-
# you must confirm the server to support HTTP/1.1 before sending it.
-
#
-
# Example:
-
# http.set_form([["q", "ruby"], ["lang", "en"]])
-
#
-
# See also RFC 2388, RFC 2616, HTML 4.01, and HTML5
-
#
-
1
def set_form(params, enctype='application/x-www-form-urlencoded', formopt={})
-
@body_data = params
-
@body = nil
-
@body_stream = nil
-
@form_option = formopt
-
case enctype
-
when /\Aapplication\/x-www-form-urlencoded\z/i,
-
/\Amultipart\/form-data\z/i
-
self.content_type = enctype
-
else
-
raise ArgumentError, "invalid enctype: #{enctype}"
-
end
-
end
-
-
# Set the Authorization: header for "Basic" authorization.
-
1
def basic_auth(account, password)
-
@header['authorization'] = [basic_encode(account, password)]
-
end
-
-
# Set Proxy-Authorization: header for "Basic" authorization.
-
1
def proxy_basic_auth(account, password)
-
@header['proxy-authorization'] = [basic_encode(account, password)]
-
end
-
-
1
def basic_encode(account, password)
-
'Basic ' + ["#{account}:#{password}"].pack('m0')
-
end
-
1
private :basic_encode
-
-
1
def connection_close?
-
6
token = /(?:\A|,)\s*close\s*(?:\z|,)/i
-
6
@header['connection']&.grep(token) {return true}
-
6
@header['proxy-connection']&.grep(token) {return true}
-
6
false
-
end
-
-
1
def connection_keep_alive?
-
token = /(?:\A|,)\s*keep-alive\s*(?:\z|,)/i
-
@header['connection']&.grep(token) {return true}
-
@header['proxy-connection']&.grep(token) {return true}
-
false
-
end
-
-
end
-
# frozen_string_literal: false
-
1
module Net::HTTP::ProxyDelta #:nodoc: internal use only
-
1
private
-
-
1
def conn_address
-
proxy_address()
-
end
-
-
1
def conn_port
-
proxy_port()
-
end
-
-
1
def edit_path(path)
-
use_ssl? ? path : "http://#{addr_port()}#{path}"
-
end
-
end
-
-
# frozen_string_literal: false
-
# HTTP request class.
-
# This class wraps together the request header and the request path.
-
# You cannot use this class directly. Instead, you should use one of its
-
# subclasses: Net::HTTP::Get, Net::HTTP::Post, Net::HTTP::Head.
-
#
-
1
class Net::HTTPRequest < Net::HTTPGenericRequest
-
# Creates an HTTP request object for +path+.
-
#
-
# +initheader+ are the default headers to use. Net::HTTP adds
-
# Accept-Encoding to enable compression of the response body unless
-
# Accept-Encoding or Range are supplied in +initheader+.
-
-
1
def initialize(path, initheader = nil)
-
3
super self.class::METHOD,
-
self.class::REQUEST_HAS_BODY,
-
self.class::RESPONSE_HAS_BODY,
-
path, initheader
-
end
-
end
-
-
# frozen_string_literal: false
-
# HTTP response class.
-
#
-
# This class wraps together the response header and the response body (the
-
# entity requested).
-
#
-
# It mixes in the HTTPHeader module, which provides access to response
-
# header values both via hash-like methods and via individual readers.
-
#
-
# Note that each possible HTTP response code defines its own
-
# HTTPResponse subclass. All classes are defined under the Net module.
-
# Indentation indicates inheritance. For a list of the classes see Net::HTTP.
-
#
-
# Correspondence <code>HTTP code => class</code> is stored in CODE_TO_OBJ
-
# constant:
-
#
-
# Net::HTTPResponse::CODE_TO_OBJ['404'] #=> Net::HTTPNotFound
-
#
-
1
class Net::HTTPResponse
-
1
class << self
-
# true if the response has a body.
-
1
def body_permitted?
-
3
self::HAS_BODY
-
end
-
-
1
def exception_type # :nodoc: internal use only
-
self::EXCEPTION_TYPE
-
end
-
-
1
def read_new(sock) #:nodoc: internal use only
-
3
httpv, code, msg = read_status_line(sock)
-
3
res = response_class(code).new(httpv, code, msg)
-
3
each_response_header(sock) do |k,v|
-
6
res.add_field k, v
-
end
-
3
res
-
end
-
-
1
private
-
-
1
def read_status_line(sock)
-
3
str = sock.readline
-
3
m = /\AHTTP(?:\/(\d+\.\d+))?\s+(\d\d\d)(?:\s+(.*))?\z/in.match(str) or
-
raise Net::HTTPBadResponse, "wrong status line: #{str.dump}"
-
3
m.captures
-
end
-
-
1
def response_class(code)
-
3
CODE_TO_OBJ[code] or
-
CODE_CLASS_TO_OBJ[code[0,1]] or
-
Net::HTTPUnknownResponse
-
end
-
-
1
def each_response_header(sock)
-
3
key = value = nil
-
3
while true
-
9
line = sock.readuntil("\n", true).sub(/\s+\z/, '')
-
9
break if line.empty?
-
6
if line[0] == ?\s or line[0] == ?\t and value
-
value << ' ' unless value.empty?
-
value << line.strip
-
else
-
6
yield key, value if key
-
6
key, value = line.strip.split(/\s*:\s*/, 2)
-
6
raise Net::HTTPBadResponse, 'wrong header line format' if value.nil?
-
end
-
end
-
3
yield key, value if key
-
end
-
end
-
-
# next is to fix bug in RDoc, where the private inside class << self
-
# spills out.
-
1
public
-
-
1
include Net::HTTPHeader
-
-
1
def initialize(httpv, code, msg) #:nodoc: internal use only
-
3
@http_version = httpv
-
3
@code = code
-
3
@message = msg
-
3
initialize_http_header nil
-
3
@body = nil
-
3
@read = false
-
3
@uri = nil
-
3
@decode_content = false
-
end
-
-
# The HTTP version supported by the server.
-
1
attr_reader :http_version
-
-
# The HTTP result code string. For example, '302'. You can also
-
# determine the response type by examining which response subclass
-
# the response object is an instance of.
-
1
attr_reader :code
-
-
# The HTTP result message sent by the server. For example, 'Not Found'.
-
1
attr_reader :message
-
1
alias msg message # :nodoc: obsolete
-
-
# The URI used to fetch this response. The response URI is only available
-
# if a URI was used to create the request.
-
1
attr_reader :uri
-
-
# Set to true automatically when the request did not contain an
-
# Accept-Encoding header from the user.
-
1
attr_accessor :decode_content
-
-
1
def inspect
-
"#<#{self.class} #{@code} #{@message} readbody=#{@read}>"
-
end
-
-
#
-
# response <-> exception relationship
-
#
-
-
1
def code_type #:nodoc:
-
self.class
-
end
-
-
1
def error! #:nodoc:
-
message = @code
-
message += ' ' + @message.dump if @message
-
raise error_type().new(message, self)
-
end
-
-
1
def error_type #:nodoc:
-
self.class::EXCEPTION_TYPE
-
end
-
-
# Raises an HTTP error if the response is not 2xx (success).
-
1
def value
-
error! unless self.kind_of?(Net::HTTPSuccess)
-
end
-
-
1
def uri= uri # :nodoc:
-
3
@uri = uri.dup if uri
-
end
-
-
#
-
# header (for backward compatibility only; DO NOT USE)
-
#
-
-
1
def response #:nodoc:
-
warn "Net::HTTPResponse#response is obsolete", uplevel: 1 if $VERBOSE
-
self
-
end
-
-
1
def header #:nodoc:
-
warn "Net::HTTPResponse#header is obsolete", uplevel: 1 if $VERBOSE
-
self
-
end
-
-
1
def read_header #:nodoc:
-
warn "Net::HTTPResponse#read_header is obsolete", uplevel: 1 if $VERBOSE
-
self
-
end
-
-
#
-
# body
-
#
-
-
1
def reading_body(sock, reqmethodallowbody) #:nodoc: internal use only
-
3
@socket = sock
-
3
@body_exist = reqmethodallowbody && self.class.body_permitted?
-
begin
-
3
yield
-
3
self.body # ensure to read body
-
ensure
-
3
@socket = nil
-
end
-
end
-
-
# Gets the entity body returned by the remote HTTP server.
-
#
-
# If a block is given, the body is passed to the block, and
-
# the body is provided in fragments, as it is read in from the socket.
-
#
-
# If +dest+ argument is given, response is read into that variable,
-
# with <code>dest#<<</code> method (it could be String or IO, or any
-
# other object responding to <code><<</code>).
-
#
-
# Calling this method a second or subsequent time for the same
-
# HTTPResponse object will return the value already read.
-
#
-
# http.request_get('/index.html') {|res|
-
# puts res.read_body
-
# }
-
#
-
# http.request_get('/index.html') {|res|
-
# p res.read_body.object_id # 538149362
-
# p res.read_body.object_id # 538149362
-
# }
-
#
-
# # using iterator
-
# http.request_get('/index.html') {|res|
-
# res.read_body do |segment|
-
# print segment
-
# end
-
# }
-
#
-
1
def read_body(dest = nil, &block)
-
9
if @read
-
6
raise IOError, "#{self.class}\#read_body called twice" if dest or block
-
6
return @body
-
end
-
3
to = procdest(dest, block)
-
3
stream_check
-
3
if @body_exist
-
3
read_body_0 to
-
3
@body = to
-
else
-
@body = nil
-
end
-
3
@read = true
-
-
3
@body
-
end
-
-
# Returns the full entity body.
-
#
-
# Calling this method a second or subsequent time will return the
-
# string already read.
-
#
-
# http.request_get('/index.html') {|res|
-
# puts res.body
-
# }
-
#
-
# http.request_get('/index.html') {|res|
-
# p res.body.object_id # 538149362
-
# p res.body.object_id # 538149362
-
# }
-
#
-
1
def body
-
9
read_body()
-
end
-
-
# Because it may be necessary to modify the body, Eg, decompression
-
# this method facilitates that.
-
1
def body=(value)
-
@body = value
-
end
-
-
1
alias entity body #:nodoc: obsolete
-
-
1
private
-
-
##
-
# Checks for a supported Content-Encoding header and yields an Inflate
-
# wrapper for this response's socket when zlib is present. If the
-
# Content-Encoding is not supported or zlib is missing, the plain socket is
-
# yielded.
-
#
-
# If a Content-Range header is present, a plain socket is yielded as the
-
# bytes in the range may not be a complete deflate block.
-
-
1
def inflater # :nodoc:
-
3
return yield @socket unless Net::HTTP::HAVE_ZLIB
-
3
return yield @socket unless @decode_content
-
3
return yield @socket if self['content-range']
-
-
3
v = self['content-encoding']
-
3
case v&.downcase
-
when 'deflate', 'gzip', 'x-gzip' then
-
self.delete 'content-encoding'
-
-
inflate_body_io = Inflater.new(@socket)
-
-
begin
-
yield inflate_body_io
-
ensure
-
orig_err = $!
-
begin
-
inflate_body_io.finish
-
rescue => err
-
raise orig_err || err
-
end
-
end
-
when 'none', 'identity' then
-
self.delete 'content-encoding'
-
-
yield @socket
-
else
-
3
yield @socket
-
end
-
end
-
-
1
def read_body_0(dest)
-
3
inflater do |inflate_body_io|
-
3
if chunked?
-
read_chunked dest, inflate_body_io
-
return
-
end
-
-
3
@socket = inflate_body_io
-
-
3
clen = content_length()
-
3
if clen
-
3
@socket.read clen, dest, true # ignore EOF
-
3
return
-
end
-
clen = range_length()
-
if clen
-
@socket.read clen, dest
-
return
-
end
-
@socket.read_all dest
-
end
-
end
-
-
##
-
# read_chunked reads from +@socket+ for chunk-size, chunk-extension, CRLF,
-
# etc. and +chunk_data_io+ for chunk-data which may be deflate or gzip
-
# encoded.
-
#
-
# See RFC 2616 section 3.6.1 for definitions
-
-
1
def read_chunked(dest, chunk_data_io) # :nodoc:
-
total = 0
-
while true
-
line = @socket.readline
-
hexlen = line.slice(/[0-9a-fA-F]+/) or
-
raise Net::HTTPBadResponse, "wrong chunk size line: #{line}"
-
len = hexlen.hex
-
break if len == 0
-
begin
-
chunk_data_io.read len, dest
-
ensure
-
total += len
-
@socket.read 2 # \r\n
-
end
-
end
-
until @socket.readline.empty?
-
# none
-
end
-
end
-
-
1
def stream_check
-
3
raise IOError, 'attempt to read body out of block' if @socket.closed?
-
end
-
-
1
def procdest(dest, block)
-
raise ArgumentError, 'both arg and block given for HTTP method' if
-
3
dest and block
-
3
if block
-
Net::ReadAdapter.new(block)
-
else
-
3
dest || ''
-
end
-
end
-
-
##
-
# Inflater is a wrapper around Net::BufferedIO that transparently inflates
-
# zlib and gzip streams.
-
-
1
class Inflater # :nodoc:
-
-
##
-
# Creates a new Inflater wrapping +socket+
-
-
1
def initialize socket
-
@socket = socket
-
# zlib with automatic gzip detection
-
@inflate = Zlib::Inflate.new(32 + Zlib::MAX_WBITS)
-
end
-
-
##
-
# Finishes the inflate stream.
-
-
1
def finish
-
return if @inflate.total_in == 0
-
@inflate.finish
-
end
-
-
##
-
# Returns a Net::ReadAdapter that inflates each read chunk into +dest+.
-
#
-
# This allows a large response body to be inflated without storing the
-
# entire body in memory.
-
-
1
def inflate_adapter(dest)
-
if dest.respond_to?(:set_encoding)
-
dest.set_encoding(Encoding::ASCII_8BIT)
-
elsif dest.respond_to?(:force_encoding)
-
dest.force_encoding(Encoding::ASCII_8BIT)
-
end
-
block = proc do |compressed_chunk|
-
@inflate.inflate(compressed_chunk) do |chunk|
-
compressed_chunk.clear
-
dest << chunk
-
end
-
end
-
-
Net::ReadAdapter.new(block)
-
end
-
-
##
-
# Reads +clen+ bytes from the socket, inflates them, then writes them to
-
# +dest+. +ignore_eof+ is passed down to Net::BufferedIO#read
-
#
-
# Unlike Net::BufferedIO#read, this method returns more than +clen+ bytes.
-
# At this time there is no way for a user of Net::HTTPResponse to read a
-
# specific number of bytes from the HTTP response body, so this internal
-
# API does not return the same number of bytes as were requested.
-
#
-
# See https://bugs.ruby-lang.org/issues/6492 for further discussion.
-
-
1
def read clen, dest, ignore_eof = false
-
temp_dest = inflate_adapter(dest)
-
-
@socket.read clen, temp_dest, ignore_eof
-
end
-
-
##
-
# Reads the rest of the socket, inflates it, then writes it to +dest+.
-
-
1
def read_all dest
-
temp_dest = inflate_adapter(dest)
-
-
@socket.read_all temp_dest
-
end
-
-
end
-
-
end
-
-
# frozen_string_literal: true
-
# :stopdoc:
-
# https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
-
1
class Net::HTTPUnknownResponse < Net::HTTPResponse
-
1
HAS_BODY = true
-
1
EXCEPTION_TYPE = Net::HTTPError
-
end
-
1
class Net::HTTPInformation < Net::HTTPResponse # 1xx
-
1
HAS_BODY = false
-
1
EXCEPTION_TYPE = Net::HTTPError
-
end
-
1
class Net::HTTPSuccess < Net::HTTPResponse # 2xx
-
1
HAS_BODY = true
-
1
EXCEPTION_TYPE = Net::HTTPError
-
end
-
1
class Net::HTTPRedirection < Net::HTTPResponse # 3xx
-
1
HAS_BODY = true
-
1
EXCEPTION_TYPE = Net::HTTPRetriableError
-
end
-
1
class Net::HTTPClientError < Net::HTTPResponse # 4xx
-
1
HAS_BODY = true
-
1
EXCEPTION_TYPE = Net::HTTPClientException # for backward compatibility
-
end
-
1
class Net::HTTPServerError < Net::HTTPResponse # 5xx
-
1
HAS_BODY = true
-
1
EXCEPTION_TYPE = Net::HTTPFatalError # for backward compatibility
-
end
-
-
1
class Net::HTTPContinue < Net::HTTPInformation # 100
-
1
HAS_BODY = false
-
end
-
1
class Net::HTTPSwitchProtocol < Net::HTTPInformation # 101
-
1
HAS_BODY = false
-
end
-
1
class Net::HTTPProcessing < Net::HTTPInformation # 102
-
1
HAS_BODY = false
-
end
-
1
class Net::HTTPEarlyHints < Net::HTTPInformation # 103 - RFC 8297
-
1
HAS_BODY = false
-
end
-
-
1
class Net::HTTPOK < Net::HTTPSuccess # 200
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPCreated < Net::HTTPSuccess # 201
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPAccepted < Net::HTTPSuccess # 202
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPNonAuthoritativeInformation < Net::HTTPSuccess # 203
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPNoContent < Net::HTTPSuccess # 204
-
1
HAS_BODY = false
-
end
-
1
class Net::HTTPResetContent < Net::HTTPSuccess # 205
-
1
HAS_BODY = false
-
end
-
1
class Net::HTTPPartialContent < Net::HTTPSuccess # 206
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPMultiStatus < Net::HTTPSuccess # 207 - RFC 4918
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPAlreadyReported < Net::HTTPSuccess # 208 - RFC 5842
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPIMUsed < Net::HTTPSuccess # 226 - RFC 3229
-
1
HAS_BODY = true
-
end
-
-
1
class Net::HTTPMultipleChoices < Net::HTTPRedirection # 300
-
1
HAS_BODY = true
-
end
-
1
Net::HTTPMultipleChoice = Net::HTTPMultipleChoices
-
1
class Net::HTTPMovedPermanently < Net::HTTPRedirection # 301
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPFound < Net::HTTPRedirection # 302
-
1
HAS_BODY = true
-
end
-
1
Net::HTTPMovedTemporarily = Net::HTTPFound
-
1
class Net::HTTPSeeOther < Net::HTTPRedirection # 303
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPNotModified < Net::HTTPRedirection # 304
-
1
HAS_BODY = false
-
end
-
1
class Net::HTTPUseProxy < Net::HTTPRedirection # 305
-
1
HAS_BODY = false
-
end
-
# 306 Switch Proxy - no longer unused
-
1
class Net::HTTPTemporaryRedirect < Net::HTTPRedirection # 307
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPPermanentRedirect < Net::HTTPRedirection # 308
-
1
HAS_BODY = true
-
end
-
-
1
class Net::HTTPBadRequest < Net::HTTPClientError # 400
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPUnauthorized < Net::HTTPClientError # 401
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPPaymentRequired < Net::HTTPClientError # 402
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPForbidden < Net::HTTPClientError # 403
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPNotFound < Net::HTTPClientError # 404
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPMethodNotAllowed < Net::HTTPClientError # 405
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPNotAcceptable < Net::HTTPClientError # 406
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPProxyAuthenticationRequired < Net::HTTPClientError # 407
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPRequestTimeout < Net::HTTPClientError # 408
-
1
HAS_BODY = true
-
end
-
1
Net::HTTPRequestTimeOut = Net::HTTPRequestTimeout
-
1
class Net::HTTPConflict < Net::HTTPClientError # 409
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPGone < Net::HTTPClientError # 410
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPLengthRequired < Net::HTTPClientError # 411
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPPreconditionFailed < Net::HTTPClientError # 412
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPPayloadTooLarge < Net::HTTPClientError # 413
-
1
HAS_BODY = true
-
end
-
1
Net::HTTPRequestEntityTooLarge = Net::HTTPPayloadTooLarge
-
1
class Net::HTTPURITooLong < Net::HTTPClientError # 414
-
1
HAS_BODY = true
-
end
-
1
Net::HTTPRequestURITooLong = Net::HTTPURITooLong
-
1
Net::HTTPRequestURITooLarge = Net::HTTPRequestURITooLong
-
1
class Net::HTTPUnsupportedMediaType < Net::HTTPClientError # 415
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPRangeNotSatisfiable < Net::HTTPClientError # 416
-
1
HAS_BODY = true
-
end
-
1
Net::HTTPRequestedRangeNotSatisfiable = Net::HTTPRangeNotSatisfiable
-
1
class Net::HTTPExpectationFailed < Net::HTTPClientError # 417
-
1
HAS_BODY = true
-
end
-
# 418 I'm a teapot - RFC 2324; a joke RFC
-
# 420 Enhance Your Calm - Twitter
-
1
class Net::HTTPMisdirectedRequest < Net::HTTPClientError # 421 - RFC 7540
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPUnprocessableEntity < Net::HTTPClientError # 422 - RFC 4918
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPLocked < Net::HTTPClientError # 423 - RFC 4918
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPFailedDependency < Net::HTTPClientError # 424 - RFC 4918
-
1
HAS_BODY = true
-
end
-
# 425 Unordered Collection - existed only in draft
-
1
class Net::HTTPUpgradeRequired < Net::HTTPClientError # 426 - RFC 2817
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPPreconditionRequired < Net::HTTPClientError # 428 - RFC 6585
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPTooManyRequests < Net::HTTPClientError # 429 - RFC 6585
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPRequestHeaderFieldsTooLarge < Net::HTTPClientError # 431 - RFC 6585
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPUnavailableForLegalReasons < Net::HTTPClientError # 451 - RFC 7725
-
1
HAS_BODY = true
-
end
-
# 444 No Response - Nginx
-
# 449 Retry With - Microsoft
-
# 450 Blocked by Windows Parental Controls - Microsoft
-
# 499 Client Closed Request - Nginx
-
-
1
class Net::HTTPInternalServerError < Net::HTTPServerError # 500
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPNotImplemented < Net::HTTPServerError # 501
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPBadGateway < Net::HTTPServerError # 502
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPServiceUnavailable < Net::HTTPServerError # 503
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPGatewayTimeout < Net::HTTPServerError # 504
-
1
HAS_BODY = true
-
end
-
1
Net::HTTPGatewayTimeOut = Net::HTTPGatewayTimeout
-
1
class Net::HTTPVersionNotSupported < Net::HTTPServerError # 505
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPVariantAlsoNegotiates < Net::HTTPServerError # 506
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPInsufficientStorage < Net::HTTPServerError # 507 - RFC 4918
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPLoopDetected < Net::HTTPServerError # 508 - RFC 5842
-
1
HAS_BODY = true
-
end
-
# 509 Bandwidth Limit Exceeded - Apache bw/limited extension
-
1
class Net::HTTPNotExtended < Net::HTTPServerError # 510 - RFC 2774
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPNetworkAuthenticationRequired < Net::HTTPServerError # 511 - RFC 6585
-
1
HAS_BODY = true
-
end
-
-
1
class Net::HTTPResponse
-
CODE_CLASS_TO_OBJ = {
-
1
'1' => Net::HTTPInformation,
-
'2' => Net::HTTPSuccess,
-
'3' => Net::HTTPRedirection,
-
'4' => Net::HTTPClientError,
-
'5' => Net::HTTPServerError
-
}
-
CODE_TO_OBJ = {
-
1
'100' => Net::HTTPContinue,
-
'101' => Net::HTTPSwitchProtocol,
-
'102' => Net::HTTPProcessing,
-
'103' => Net::HTTPEarlyHints,
-
-
'200' => Net::HTTPOK,
-
'201' => Net::HTTPCreated,
-
'202' => Net::HTTPAccepted,
-
'203' => Net::HTTPNonAuthoritativeInformation,
-
'204' => Net::HTTPNoContent,
-
'205' => Net::HTTPResetContent,
-
'206' => Net::HTTPPartialContent,
-
'207' => Net::HTTPMultiStatus,
-
'208' => Net::HTTPAlreadyReported,
-
'226' => Net::HTTPIMUsed,
-
-
'300' => Net::HTTPMultipleChoices,
-
'301' => Net::HTTPMovedPermanently,
-
'302' => Net::HTTPFound,
-
'303' => Net::HTTPSeeOther,
-
'304' => Net::HTTPNotModified,
-
'305' => Net::HTTPUseProxy,
-
'307' => Net::HTTPTemporaryRedirect,
-
'308' => Net::HTTPPermanentRedirect,
-
-
'400' => Net::HTTPBadRequest,
-
'401' => Net::HTTPUnauthorized,
-
'402' => Net::HTTPPaymentRequired,
-
'403' => Net::HTTPForbidden,
-
'404' => Net::HTTPNotFound,
-
'405' => Net::HTTPMethodNotAllowed,
-
'406' => Net::HTTPNotAcceptable,
-
'407' => Net::HTTPProxyAuthenticationRequired,
-
'408' => Net::HTTPRequestTimeout,
-
'409' => Net::HTTPConflict,
-
'410' => Net::HTTPGone,
-
'411' => Net::HTTPLengthRequired,
-
'412' => Net::HTTPPreconditionFailed,
-
'413' => Net::HTTPPayloadTooLarge,
-
'414' => Net::HTTPURITooLong,
-
'415' => Net::HTTPUnsupportedMediaType,
-
'416' => Net::HTTPRangeNotSatisfiable,
-
'417' => Net::HTTPExpectationFailed,
-
'421' => Net::HTTPMisdirectedRequest,
-
'422' => Net::HTTPUnprocessableEntity,
-
'423' => Net::HTTPLocked,
-
'424' => Net::HTTPFailedDependency,
-
'426' => Net::HTTPUpgradeRequired,
-
'428' => Net::HTTPPreconditionRequired,
-
'429' => Net::HTTPTooManyRequests,
-
'431' => Net::HTTPRequestHeaderFieldsTooLarge,
-
'451' => Net::HTTPUnavailableForLegalReasons,
-
-
'500' => Net::HTTPInternalServerError,
-
'501' => Net::HTTPNotImplemented,
-
'502' => Net::HTTPBadGateway,
-
'503' => Net::HTTPServiceUnavailable,
-
'504' => Net::HTTPGatewayTimeout,
-
'505' => Net::HTTPVersionNotSupported,
-
'506' => Net::HTTPVariantAlsoNegotiates,
-
'507' => Net::HTTPInsufficientStorage,
-
'508' => Net::HTTPLoopDetected,
-
'510' => Net::HTTPNotExtended,
-
'511' => Net::HTTPNetworkAuthenticationRequired,
-
}
-
end
-
-
# :startdoc:
-
# frozen_string_literal: true
-
#
-
# = net/protocol.rb
-
#
-
#--
-
# Copyright (c) 1999-2004 Yukihiro Matsumoto
-
# Copyright (c) 1999-2004 Minero Aoki
-
#
-
# written and maintained by Minero Aoki <aamine@loveruby.net>
-
#
-
# This program is free software. You can re-distribute and/or
-
# modify this program under the same terms as Ruby itself,
-
# Ruby Distribute License or GNU General Public License.
-
#
-
# $Id$
-
#++
-
#
-
# WARNING: This file is going to remove.
-
# Do not rely on the implementation written in this file.
-
#
-
-
1
require 'socket'
-
1
require 'timeout'
-
1
require 'io/wait'
-
-
1
module Net # :nodoc:
-
-
1
class Protocol #:nodoc: internal use only
-
1
private
-
1
def Protocol.protocol_param(name, val)
-
module_eval(<<-End, __FILE__, __LINE__ + 1)
-
def #{name}
-
#{val}
-
end
-
End
-
end
-
-
1
def ssl_socket_connect(s, timeout)
-
if timeout
-
while true
-
raise Net::OpenTimeout if timeout <= 0
-
start = Process.clock_gettime Process::CLOCK_MONOTONIC
-
# to_io is required because SSLSocket doesn't have wait_readable yet
-
case s.connect_nonblock(exception: false)
-
when :wait_readable; s.to_io.wait_readable(timeout)
-
when :wait_writable; s.to_io.wait_writable(timeout)
-
else; break
-
end
-
timeout -= Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
-
end
-
else
-
s.connect
-
end
-
end
-
end
-
-
-
1
class ProtocolError < StandardError; end
-
1
class ProtoSyntaxError < ProtocolError; end
-
1
class ProtoFatalError < ProtocolError; end
-
1
class ProtoUnknownError < ProtocolError; end
-
1
class ProtoServerError < ProtocolError; end
-
1
class ProtoAuthError < ProtocolError; end
-
1
class ProtoCommandError < ProtocolError; end
-
1
class ProtoRetriableError < ProtocolError; end
-
1
ProtocRetryError = ProtoRetriableError
-
-
##
-
# OpenTimeout, a subclass of Timeout::Error, is raised if a connection cannot
-
# be created within the open_timeout.
-
-
1
class OpenTimeout < Timeout::Error; end
-
-
##
-
# ReadTimeout, a subclass of Timeout::Error, is raised if a chunk of the
-
# response cannot be read within the read_timeout.
-
-
1
class ReadTimeout < Timeout::Error
-
1
def initialize(io = nil)
-
@io = io
-
end
-
1
attr_reader :io
-
-
1
def message
-
msg = super
-
if @io
-
msg = "#{msg} with #{@io.inspect}"
-
end
-
msg
-
end
-
end
-
-
##
-
# WriteTimeout, a subclass of Timeout::Error, is raised if a chunk of the
-
# response cannot be written within the write_timeout. Not raised on Windows.
-
-
1
class WriteTimeout < Timeout::Error
-
1
def initialize(io = nil)
-
@io = io
-
end
-
1
attr_reader :io
-
-
1
def message
-
msg = super
-
if @io
-
msg = "#{msg} with #{@io.inspect}"
-
end
-
msg
-
end
-
end
-
-
-
1
class BufferedIO #:nodoc: internal use only
-
1
def initialize(io, read_timeout: 60, write_timeout: 60, continue_timeout: nil, debug_output: nil)
-
3
@io = io
-
3
@read_timeout = read_timeout
-
3
@write_timeout = write_timeout
-
3
@continue_timeout = continue_timeout
-
3
@debug_output = debug_output
-
3
@rbuf = ''.b
-
end
-
-
1
attr_reader :io
-
1
attr_accessor :read_timeout
-
1
attr_accessor :write_timeout
-
1
attr_accessor :continue_timeout
-
1
attr_accessor :debug_output
-
-
1
def inspect
-
"#<#{self.class} io=#{@io}>"
-
end
-
-
1
def eof?
-
@io.eof?
-
end
-
-
1
def closed?
-
9
@io.closed?
-
end
-
-
1
def close
-
3
@io.close
-
end
-
-
#
-
# Read
-
#
-
-
1
public
-
-
1
def read(len, dest = ''.b, ignore_eof = false)
-
3
LOG "reading #{len} bytes..."
-
3
read_bytes = 0
-
begin
-
3
while read_bytes + @rbuf.size < len
-
s = rbuf_consume(@rbuf.size)
-
read_bytes += s.size
-
dest << s
-
rbuf_fill
-
end
-
3
s = rbuf_consume(len - read_bytes)
-
3
read_bytes += s.size
-
3
dest << s
-
rescue EOFError
-
raise unless ignore_eof
-
end
-
3
LOG "read #{read_bytes} bytes"
-
3
dest
-
end
-
-
1
def read_all(dest = ''.b)
-
LOG 'reading all...'
-
read_bytes = 0
-
begin
-
while true
-
s = rbuf_consume(@rbuf.size)
-
read_bytes += s.size
-
dest << s
-
rbuf_fill
-
end
-
rescue EOFError
-
;
-
end
-
LOG "read #{read_bytes} bytes"
-
dest
-
end
-
-
1
def readuntil(terminator, ignore_eof = false)
-
begin
-
12
until idx = @rbuf.index(terminator)
-
3
rbuf_fill
-
end
-
12
return rbuf_consume(idx + terminator.size)
-
rescue EOFError
-
raise unless ignore_eof
-
return rbuf_consume(@rbuf.size)
-
end
-
end
-
-
1
def readline
-
3
readuntil("\n").chop
-
end
-
-
1
private
-
-
1
BUFSIZE = 1024 * 16
-
-
1
def rbuf_fill
-
3
tmp = @rbuf.empty? ? @rbuf : nil
-
3
case rv = @io.read_nonblock(BUFSIZE, tmp, exception: false)
-
when String
-
3
return if rv.equal?(tmp)
-
@rbuf << rv
-
rv.clear
-
return
-
when :wait_readable
-
(io = @io.to_io).wait_readable(@read_timeout) or raise Net::ReadTimeout.new(io)
-
# continue looping
-
when :wait_writable
-
# OpenSSL::Buffering#read_nonblock may fail with IO::WaitWritable.
-
# http://www.openssl.org/support/faq.html#PROG10
-
(io = @io.to_io).wait_writable(@read_timeout) or raise Net::ReadTimeout.new(io)
-
# continue looping
-
when nil
-
raise EOFError, 'end of file reached'
-
end while true
-
end
-
-
1
def rbuf_consume(len)
-
15
if len == @rbuf.size
-
3
s = @rbuf
-
3
@rbuf = ''.b
-
else
-
12
s = @rbuf.slice!(0, len)
-
end
-
15
@debug_output << %Q[-> #{s.dump}\n] if @debug_output
-
15
s
-
end
-
-
#
-
# Write
-
#
-
-
1
public
-
-
1
def write(*strs)
-
6
writing {
-
6
write0(*strs)
-
}
-
end
-
-
1
alias << write
-
-
1
def writeline(str)
-
writing {
-
write0 str + "\r\n"
-
}
-
end
-
-
1
private
-
-
1
def writing
-
6
@written_bytes = 0
-
6
@debug_output << '<- ' if @debug_output
-
6
yield
-
6
@debug_output << "\n" if @debug_output
-
6
bytes = @written_bytes
-
6
@written_bytes = nil
-
6
bytes
-
end
-
-
1
def write0(*strs)
-
6
@debug_output << strs.map(&:dump).join if @debug_output
-
6
orig_written_bytes = @written_bytes
-
6
strs.each_with_index do |str, i|
-
6
need_retry = true
-
6
case len = @io.write_nonblock(str, exception: false)
-
when Integer
-
6
@written_bytes += len
-
6
len -= str.bytesize
-
6
if len == 0
-
6
if strs.size == i+1
-
6
return @written_bytes - orig_written_bytes
-
else
-
need_retry = false
-
# next string
-
end
-
elsif len < 0
-
str = str.byteslice(len, -len)
-
else # len > 0
-
need_retry = false
-
# next string
-
end
-
# continue looping
-
when :wait_writable
-
(io = @io.to_io).wait_writable(@write_timeout) or raise Net::WriteTimeout.new(io)
-
# continue looping
-
end while need_retry
-
end
-
end
-
-
#
-
# Logging
-
#
-
-
1
private
-
-
1
def LOG_off
-
@save_debug_out = @debug_output
-
@debug_output = nil
-
end
-
-
1
def LOG_on
-
@debug_output = @save_debug_out
-
end
-
-
1
def LOG(msg)
-
6
return unless @debug_output
-
@debug_output << msg + "\n"
-
end
-
end
-
-
-
1
class InternetMessageIO < BufferedIO #:nodoc: internal use only
-
1
def initialize(*, **)
-
super
-
@wbuf = nil
-
end
-
-
#
-
# Read
-
#
-
-
1
def each_message_chunk
-
LOG 'reading message...'
-
LOG_off()
-
read_bytes = 0
-
while (line = readuntil("\r\n")) != ".\r\n"
-
read_bytes += line.size
-
yield line.delete_prefix('.')
-
end
-
LOG_on()
-
LOG "read message (#{read_bytes} bytes)"
-
end
-
-
# *library private* (cannot handle 'break')
-
1
def each_list_item
-
while (str = readuntil("\r\n")) != ".\r\n"
-
yield str.chop
-
end
-
end
-
-
1
def write_message_0(src)
-
prev = @written_bytes
-
each_crlf_line(src) do |line|
-
write0 dot_stuff(line)
-
end
-
@written_bytes - prev
-
end
-
-
#
-
# Write
-
#
-
-
1
def write_message(src)
-
LOG "writing message from #{src.class}"
-
LOG_off()
-
len = writing {
-
using_each_crlf_line {
-
write_message_0 src
-
}
-
}
-
LOG_on()
-
LOG "wrote #{len} bytes"
-
len
-
end
-
-
1
def write_message_by_block(&block)
-
LOG 'writing message from block'
-
LOG_off()
-
len = writing {
-
using_each_crlf_line {
-
begin
-
block.call(WriteAdapter.new(self, :write_message_0))
-
rescue LocalJumpError
-
# allow `break' from writer block
-
end
-
}
-
}
-
LOG_on()
-
LOG "wrote #{len} bytes"
-
len
-
end
-
-
1
private
-
-
1
def dot_stuff(s)
-
s.sub(/\A\./, '..')
-
end
-
-
1
def using_each_crlf_line
-
@wbuf = ''.b
-
yield
-
if not @wbuf.empty? # unterminated last line
-
write0 dot_stuff(@wbuf.chomp) + "\r\n"
-
elsif @written_bytes == 0 # empty src
-
write0 "\r\n"
-
end
-
write0 ".\r\n"
-
@wbuf = nil
-
end
-
-
1
def each_crlf_line(src)
-
buffer_filling(@wbuf, src) do
-
while line = @wbuf.slice!(/\A[^\r\n]*(?:\n|\r(?:\n|(?!\z)))/)
-
yield line.chomp("\n") + "\r\n"
-
end
-
end
-
end
-
-
1
def buffer_filling(buf, src)
-
case src
-
when String # for speeding up.
-
0.step(src.size - 1, 1024) do |i|
-
buf << src[i, 1024]
-
yield
-
end
-
when File # for speeding up.
-
while s = src.read(1024)
-
buf << s
-
yield
-
end
-
else # generic reader
-
src.each do |str|
-
buf << str
-
yield if buf.size > 1024
-
end
-
yield unless buf.empty?
-
end
-
end
-
end
-
-
-
#
-
# The writer adapter class
-
#
-
1
class WriteAdapter
-
1
def initialize(socket, method)
-
@socket = socket
-
@method_id = method
-
end
-
-
1
def inspect
-
"#<#{self.class} socket=#{@socket.inspect}>"
-
end
-
-
1
def write(str)
-
@socket.__send__(@method_id, str)
-
end
-
-
1
alias print write
-
-
1
def <<(str)
-
write str
-
self
-
end
-
-
1
def puts(str = '')
-
write str.chomp("\n") + "\n"
-
end
-
-
1
def printf(*args)
-
write sprintf(*args)
-
end
-
end
-
-
-
1
class ReadAdapter #:nodoc: internal use only
-
1
def initialize(block)
-
@block = block
-
end
-
-
1
def inspect
-
"#<#{self.class}>"
-
end
-
-
1
def <<(str)
-
call_block(str, &@block) if @block
-
end
-
-
1
private
-
-
# This method is needed because @block must be called by yield,
-
# not Proc#call. You can see difference when using `break' in
-
# the block.
-
1
def call_block(str)
-
yield str
-
end
-
end
-
-
-
1
module NetPrivate #:nodoc: obsolete
-
1
Socket = ::Net::InternetMessageIO
-
end
-
-
end # module Net
-
# frozen_string_literal: true
-
-
#
-
# = open3.rb: Popen, but with stderr, too
-
#
-
# Author:: Yukihiro Matsumoto
-
# Documentation:: Konrad Meyer
-
#
-
# Open3 gives you access to stdin, stdout, and stderr when running other
-
# programs.
-
#
-
-
#
-
# Open3 grants you access to stdin, stdout, stderr and a thread to wait for the
-
# child process when running another program.
-
# You can specify various attributes, redirections, current directory, etc., of
-
# the program in the same way as for Process.spawn.
-
#
-
# - Open3.popen3 : pipes for stdin, stdout, stderr
-
# - Open3.popen2 : pipes for stdin, stdout
-
# - Open3.popen2e : pipes for stdin, merged stdout and stderr
-
# - Open3.capture3 : give a string for stdin; get strings for stdout, stderr
-
# - Open3.capture2 : give a string for stdin; get a string for stdout
-
# - Open3.capture2e : give a string for stdin; get a string for merged stdout and stderr
-
# - Open3.pipeline_rw : pipes for first stdin and last stdout of a pipeline
-
# - Open3.pipeline_r : pipe for last stdout of a pipeline
-
# - Open3.pipeline_w : pipe for first stdin of a pipeline
-
# - Open3.pipeline_start : run a pipeline without waiting
-
# - Open3.pipeline : run a pipeline and wait for its completion
-
#
-
-
1
module Open3
-
-
# Open stdin, stdout, and stderr streams and start external executable.
-
# In addition, a thread to wait for the started process is created.
-
# The thread has a pid method and a thread variable :pid which is the pid of
-
# the started process.
-
#
-
# Block form:
-
#
-
# Open3.popen3([env,] cmd... [, opts]) {|stdin, stdout, stderr, wait_thr|
-
# pid = wait_thr.pid # pid of the started process.
-
# ...
-
# exit_status = wait_thr.value # Process::Status object returned.
-
# }
-
#
-
# Non-block form:
-
#
-
# stdin, stdout, stderr, wait_thr = Open3.popen3([env,] cmd... [, opts])
-
# pid = wait_thr[:pid] # pid of the started process
-
# ...
-
# stdin.close # stdin, stdout and stderr should be closed explicitly in this form.
-
# stdout.close
-
# stderr.close
-
# exit_status = wait_thr.value # Process::Status object returned.
-
#
-
# The parameters env, cmd, and opts are passed to Process.spawn.
-
# A commandline string and a list of argument strings can be accepted as follows:
-
#
-
# Open3.popen3("echo abc") {|i, o, e, t| ... }
-
# Open3.popen3("echo", "abc") {|i, o, e, t| ... }
-
# Open3.popen3(["echo", "argv0"], "abc") {|i, o, e, t| ... }
-
#
-
# If the last parameter, opts, is a Hash, it is recognized as an option for Process.spawn.
-
#
-
# Open3.popen3("pwd", :chdir=>"/") {|i,o,e,t|
-
# p o.read.chomp #=> "/"
-
# }
-
#
-
# wait_thr.value waits for the termination of the process.
-
# The block form also waits for the process when it returns.
-
#
-
# Closing stdin, stdout and stderr does not wait for the process to complete.
-
#
-
# You should be careful to avoid deadlocks.
-
# Since pipes are fixed length buffers,
-
# Open3.popen3("prog") {|i, o, e, t| o.read } deadlocks if
-
# the program generates too much output on stderr.
-
# You should read stdout and stderr simultaneously (using threads or IO.select).
-
# However, if you don't need stderr output, you can use Open3.popen2.
-
# If merged stdout and stderr output is not a problem, you can use Open3.popen2e.
-
# If you really need stdout and stderr output as separate strings, you can consider Open3.capture3.
-
#
-
1
def popen3(*cmd, &block)
-
3
if Hash === cmd.last
-
3
opts = cmd.pop.dup
-
else
-
opts = {}
-
end
-
-
3
in_r, in_w = IO.pipe
-
3
opts[:in] = in_r
-
3
in_w.sync = true
-
-
3
out_r, out_w = IO.pipe
-
3
opts[:out] = out_w
-
-
3
err_r, err_w = IO.pipe
-
3
opts[:err] = err_w
-
-
3
popen_run(cmd, opts, [in_r, out_w, err_w], [in_w, out_r, err_r], &block)
-
end
-
1
module_function :popen3
-
-
# Open3.popen2 is similar to Open3.popen3 except that it doesn't create a pipe for
-
# the standard error stream.
-
#
-
# Block form:
-
#
-
# Open3.popen2([env,] cmd... [, opts]) {|stdin, stdout, wait_thr|
-
# pid = wait_thr.pid # pid of the started process.
-
# ...
-
# exit_status = wait_thr.value # Process::Status object returned.
-
# }
-
#
-
# Non-block form:
-
#
-
# stdin, stdout, wait_thr = Open3.popen2([env,] cmd... [, opts])
-
# ...
-
# stdin.close # stdin and stdout should be closed explicitly in this form.
-
# stdout.close
-
#
-
# See Process.spawn for the optional hash arguments _env_ and _opts_.
-
#
-
# Example:
-
#
-
# Open3.popen2("wc -c") {|i,o,t|
-
# i.print "answer to life the universe and everything"
-
# i.close
-
# p o.gets #=> "42\n"
-
# }
-
#
-
# Open3.popen2("bc -q") {|i,o,t|
-
# i.puts "obase=13"
-
# i.puts "6 * 9"
-
# p o.gets #=> "42\n"
-
# }
-
#
-
# Open3.popen2("dc") {|i,o,t|
-
# i.print "42P"
-
# i.close
-
# p o.read #=> "*"
-
# }
-
#
-
1
def popen2(*cmd, &block)
-
if Hash === cmd.last
-
opts = cmd.pop.dup
-
else
-
opts = {}
-
end
-
-
in_r, in_w = IO.pipe
-
opts[:in] = in_r
-
in_w.sync = true
-
-
out_r, out_w = IO.pipe
-
opts[:out] = out_w
-
-
popen_run(cmd, opts, [in_r, out_w], [in_w, out_r], &block)
-
end
-
1
module_function :popen2
-
-
# Open3.popen2e is similar to Open3.popen3 except that it merges
-
# the standard output stream and the standard error stream.
-
#
-
# Block form:
-
#
-
# Open3.popen2e([env,] cmd... [, opts]) {|stdin, stdout_and_stderr, wait_thr|
-
# pid = wait_thr.pid # pid of the started process.
-
# ...
-
# exit_status = wait_thr.value # Process::Status object returned.
-
# }
-
#
-
# Non-block form:
-
#
-
# stdin, stdout_and_stderr, wait_thr = Open3.popen2e([env,] cmd... [, opts])
-
# ...
-
# stdin.close # stdin and stdout_and_stderr should be closed explicitly in this form.
-
# stdout_and_stderr.close
-
#
-
# See Process.spawn for the optional hash arguments _env_ and _opts_.
-
#
-
# Example:
-
# # check gcc warnings
-
# source = "foo.c"
-
# Open3.popen2e("gcc", "-Wall", source) {|i,oe,t|
-
# oe.each {|line|
-
# if /warning/ =~ line
-
# ...
-
# end
-
# }
-
# }
-
#
-
1
def popen2e(*cmd, &block)
-
if Hash === cmd.last
-
opts = cmd.pop.dup
-
else
-
opts = {}
-
end
-
-
in_r, in_w = IO.pipe
-
opts[:in] = in_r
-
in_w.sync = true
-
-
out_r, out_w = IO.pipe
-
opts[[:out, :err]] = out_w
-
-
popen_run(cmd, opts, [in_r, out_w], [in_w, out_r], &block)
-
end
-
1
module_function :popen2e
-
-
1
def popen_run(cmd, opts, child_io, parent_io) # :nodoc:
-
3
pid = spawn(*cmd, opts)
-
2
wait_thr = Process.detach(pid)
-
2
child_io.each(&:close)
-
2
result = [*parent_io, wait_thr]
-
2
if defined? yield
-
begin
-
2
return yield(*result)
-
ensure
-
2
parent_io.each(&:close)
-
2
wait_thr.join
-
end
-
end
-
result
-
end
-
1
module_function :popen_run
-
1
class << self
-
1
private :popen_run
-
end
-
-
# Open3.capture3 captures the standard output and the standard error of a command.
-
#
-
# stdout_str, stderr_str, status = Open3.capture3([env,] cmd... [, opts])
-
#
-
# The arguments env, cmd and opts are passed to Open3.popen3 except
-
# <code>opts[:stdin_data]</code> and <code>opts[:binmode]</code>. See Process.spawn.
-
#
-
# If <code>opts[:stdin_data]</code> is specified, it is sent to the command's standard input.
-
#
-
# If <code>opts[:binmode]</code> is true, internal pipes are set to binary mode.
-
#
-
# Examples:
-
#
-
# # dot is a command of graphviz.
-
# graph = <<'End'
-
# digraph g {
-
# a -> b
-
# }
-
# End
-
# drawn_graph, dot_log = Open3.capture3("dot -v", :stdin_data=>graph)
-
#
-
# o, e, s = Open3.capture3("echo abc; sort >&2", :stdin_data=>"foo\nbar\nbaz\n")
-
# p o #=> "abc\n"
-
# p e #=> "bar\nbaz\nfoo\n"
-
# p s #=> #<Process::Status: pid 32682 exit 0>
-
#
-
# # generate a thumbnail image using the convert command of ImageMagick.
-
# # However, if the image is really stored in a file,
-
# # system("convert", "-thumbnail", "80", "png:#{filename}", "png:-") is better
-
# # because of reduced memory consumption.
-
# # But if the image is stored in a DB or generated by the gnuplot Open3.capture2 example,
-
# # Open3.capture3 should be considered.
-
# #
-
# image = File.read("/usr/share/openclipart/png/animals/mammals/sheep-md-v0.1.png", :binmode=>true)
-
# thumbnail, err, s = Open3.capture3("convert -thumbnail 80 png:- png:-", :stdin_data=>image, :binmode=>true)
-
# if s.success?
-
# STDOUT.binmode; print thumbnail
-
# end
-
#
-
1
def capture3(*cmd)
-
3
if Hash === cmd.last
-
opts = cmd.pop.dup
-
else
-
3
opts = {}
-
end
-
-
3
stdin_data = opts.delete(:stdin_data) || ''
-
3
binmode = opts.delete(:binmode)
-
-
3
popen3(*cmd, opts) {|i, o, e, t|
-
2
if binmode
-
i.binmode
-
o.binmode
-
e.binmode
-
end
-
4
out_reader = Thread.new { o.read }
-
4
err_reader = Thread.new { e.read }
-
begin
-
2
if stdin_data.respond_to? :readpartial
-
IO.copy_stream(stdin_data, i)
-
else
-
2
i.write stdin_data
-
end
-
rescue Errno::EPIPE
-
end
-
2
i.close
-
2
[out_reader.value, err_reader.value, t.value]
-
}
-
end
-
1
module_function :capture3
-
-
# Open3.capture2 captures the standard output of a command.
-
#
-
# stdout_str, status = Open3.capture2([env,] cmd... [, opts])
-
#
-
# The arguments env, cmd and opts are passed to Open3.popen3 except
-
# <code>opts[:stdin_data]</code> and <code>opts[:binmode]</code>. See Process.spawn.
-
#
-
# If <code>opts[:stdin_data]</code> is specified, it is sent to the command's standard input.
-
#
-
# If <code>opts[:binmode]</code> is true, internal pipes are set to binary mode.
-
#
-
# Example:
-
#
-
# # factor is a command for integer factorization.
-
# o, s = Open3.capture2("factor", :stdin_data=>"42")
-
# p o #=> "42: 2 3 7\n"
-
#
-
# # generate x**2 graph in png using gnuplot.
-
# gnuplot_commands = <<"End"
-
# set terminal png
-
# plot x**2, "-" with lines
-
# 1 14
-
# 2 1
-
# 3 8
-
# 4 5
-
# e
-
# End
-
# image, s = Open3.capture2("gnuplot", :stdin_data=>gnuplot_commands, :binmode=>true)
-
#
-
1
def capture2(*cmd)
-
if Hash === cmd.last
-
opts = cmd.pop.dup
-
else
-
opts = {}
-
end
-
-
stdin_data = opts.delete(:stdin_data)
-
binmode = opts.delete(:binmode)
-
-
popen2(*cmd, opts) {|i, o, t|
-
if binmode
-
i.binmode
-
o.binmode
-
end
-
out_reader = Thread.new { o.read }
-
if stdin_data
-
begin
-
if stdin_data.respond_to? :readpartial
-
IO.copy_stream(stdin_data, i)
-
else
-
i.write stdin_data
-
end
-
rescue Errno::EPIPE
-
end
-
end
-
i.close
-
[out_reader.value, t.value]
-
}
-
end
-
1
module_function :capture2
-
-
# Open3.capture2e captures the standard output and the standard error of a command.
-
#
-
# stdout_and_stderr_str, status = Open3.capture2e([env,] cmd... [, opts])
-
#
-
# The arguments env, cmd and opts are passed to Open3.popen3 except
-
# <code>opts[:stdin_data]</code> and <code>opts[:binmode]</code>. See Process.spawn.
-
#
-
# If <code>opts[:stdin_data]</code> is specified, it is sent to the command's standard input.
-
#
-
# If <code>opts[:binmode]</code> is true, internal pipes are set to binary mode.
-
#
-
# Example:
-
#
-
# # capture make log
-
# make_log, s = Open3.capture2e("make")
-
#
-
1
def capture2e(*cmd)
-
if Hash === cmd.last
-
opts = cmd.pop.dup
-
else
-
opts = {}
-
end
-
-
stdin_data = opts.delete(:stdin_data)
-
binmode = opts.delete(:binmode)
-
-
popen2e(*cmd, opts) {|i, oe, t|
-
if binmode
-
i.binmode
-
oe.binmode
-
end
-
outerr_reader = Thread.new { oe.read }
-
if stdin_data
-
begin
-
if stdin_data.respond_to? :readpartial
-
IO.copy_stream(stdin_data, i)
-
else
-
i.write stdin_data
-
end
-
rescue Errno::EPIPE
-
end
-
end
-
i.close
-
[outerr_reader.value, t.value]
-
}
-
end
-
1
module_function :capture2e
-
-
# Open3.pipeline_rw starts a list of commands as a pipeline with pipes
-
# which connect to stdin of the first command and stdout of the last command.
-
#
-
# Open3.pipeline_rw(cmd1, cmd2, ... [, opts]) {|first_stdin, last_stdout, wait_threads|
-
# ...
-
# }
-
#
-
# first_stdin, last_stdout, wait_threads = Open3.pipeline_rw(cmd1, cmd2, ... [, opts])
-
# ...
-
# first_stdin.close
-
# last_stdout.close
-
#
-
# Each cmd is a string or an array.
-
# If it is an array, the elements are passed to Process.spawn.
-
#
-
# cmd:
-
# commandline command line string which is passed to a shell
-
# [env, commandline, opts] command line string which is passed to a shell
-
# [env, cmdname, arg1, ..., opts] command name and one or more arguments (no shell)
-
# [env, [cmdname, argv0], arg1, ..., opts] command name and arguments including argv[0] (no shell)
-
#
-
# Note that env and opts are optional, as for Process.spawn.
-
#
-
# The options to pass to Process.spawn are constructed by merging
-
# +opts+, the last hash element of the array, and
-
# specifications for the pipes between each of the commands.
-
#
-
# Example:
-
#
-
# Open3.pipeline_rw("tr -dc A-Za-z", "wc -c") {|i, o, ts|
-
# i.puts "All persons more than a mile high to leave the court."
-
# i.close
-
# p o.gets #=> "42\n"
-
# }
-
#
-
# Open3.pipeline_rw("sort", "cat -n") {|stdin, stdout, wait_thrs|
-
# stdin.puts "foo"
-
# stdin.puts "bar"
-
# stdin.puts "baz"
-
# stdin.close # send EOF to sort.
-
# p stdout.read #=> " 1\tbar\n 2\tbaz\n 3\tfoo\n"
-
# }
-
1
def pipeline_rw(*cmds, &block)
-
if Hash === cmds.last
-
opts = cmds.pop.dup
-
else
-
opts = {}
-
end
-
-
in_r, in_w = IO.pipe
-
opts[:in] = in_r
-
in_w.sync = true
-
-
out_r, out_w = IO.pipe
-
opts[:out] = out_w
-
-
pipeline_run(cmds, opts, [in_r, out_w], [in_w, out_r], &block)
-
end
-
1
module_function :pipeline_rw
-
-
# Open3.pipeline_r starts a list of commands as a pipeline with a pipe
-
# which connects to stdout of the last command.
-
#
-
# Open3.pipeline_r(cmd1, cmd2, ... [, opts]) {|last_stdout, wait_threads|
-
# ...
-
# }
-
#
-
# last_stdout, wait_threads = Open3.pipeline_r(cmd1, cmd2, ... [, opts])
-
# ...
-
# last_stdout.close
-
#
-
# Each cmd is a string or an array.
-
# If it is an array, the elements are passed to Process.spawn.
-
#
-
# cmd:
-
# commandline command line string which is passed to a shell
-
# [env, commandline, opts] command line string which is passed to a shell
-
# [env, cmdname, arg1, ..., opts] command name and one or more arguments (no shell)
-
# [env, [cmdname, argv0], arg1, ..., opts] command name and arguments including argv[0] (no shell)
-
#
-
# Note that env and opts are optional, as for Process.spawn.
-
#
-
# Example:
-
#
-
# Open3.pipeline_r("zcat /var/log/apache2/access.log.*.gz",
-
# [{"LANG"=>"C"}, "grep", "GET /favicon.ico"],
-
# "logresolve") {|o, ts|
-
# o.each_line {|line|
-
# ...
-
# }
-
# }
-
#
-
# Open3.pipeline_r("yes", "head -10") {|o, ts|
-
# p o.read #=> "y\ny\ny\ny\ny\ny\ny\ny\ny\ny\n"
-
# p ts[0].value #=> #<Process::Status: pid 24910 SIGPIPE (signal 13)>
-
# p ts[1].value #=> #<Process::Status: pid 24913 exit 0>
-
# }
-
#
-
1
def pipeline_r(*cmds, &block)
-
if Hash === cmds.last
-
opts = cmds.pop.dup
-
else
-
opts = {}
-
end
-
-
out_r, out_w = IO.pipe
-
opts[:out] = out_w
-
-
pipeline_run(cmds, opts, [out_w], [out_r], &block)
-
end
-
1
module_function :pipeline_r
-
-
# Open3.pipeline_w starts a list of commands as a pipeline with a pipe
-
# which connects to stdin of the first command.
-
#
-
# Open3.pipeline_w(cmd1, cmd2, ... [, opts]) {|first_stdin, wait_threads|
-
# ...
-
# }
-
#
-
# first_stdin, wait_threads = Open3.pipeline_w(cmd1, cmd2, ... [, opts])
-
# ...
-
# first_stdin.close
-
#
-
# Each cmd is a string or an array.
-
# If it is an array, the elements are passed to Process.spawn.
-
#
-
# cmd:
-
# commandline command line string which is passed to a shell
-
# [env, commandline, opts] command line string which is passed to a shell
-
# [env, cmdname, arg1, ..., opts] command name and one or more arguments (no shell)
-
# [env, [cmdname, argv0], arg1, ..., opts] command name and arguments including argv[0] (no shell)
-
#
-
# Note that env and opts are optional, as for Process.spawn.
-
#
-
# Example:
-
#
-
# Open3.pipeline_w("bzip2 -c", :out=>"/tmp/hello.bz2") {|i, ts|
-
# i.puts "hello"
-
# }
-
#
-
1
def pipeline_w(*cmds, &block)
-
if Hash === cmds.last
-
opts = cmds.pop.dup
-
else
-
opts = {}
-
end
-
-
in_r, in_w = IO.pipe
-
opts[:in] = in_r
-
in_w.sync = true
-
-
pipeline_run(cmds, opts, [in_r], [in_w], &block)
-
end
-
1
module_function :pipeline_w
-
-
# Open3.pipeline_start starts a list of commands as a pipeline.
-
# No pipes are created for stdin of the first command and
-
# stdout of the last command.
-
#
-
# Open3.pipeline_start(cmd1, cmd2, ... [, opts]) {|wait_threads|
-
# ...
-
# }
-
#
-
# wait_threads = Open3.pipeline_start(cmd1, cmd2, ... [, opts])
-
# ...
-
#
-
# Each cmd is a string or an array.
-
# If it is an array, the elements are passed to Process.spawn.
-
#
-
# cmd:
-
# commandline command line string which is passed to a shell
-
# [env, commandline, opts] command line string which is passed to a shell
-
# [env, cmdname, arg1, ..., opts] command name and one or more arguments (no shell)
-
# [env, [cmdname, argv0], arg1, ..., opts] command name and arguments including argv[0] (no shell)
-
#
-
# Note that env and opts are optional, as for Process.spawn.
-
#
-
# Example:
-
#
-
# # Run xeyes in 10 seconds.
-
# Open3.pipeline_start("xeyes") {|ts|
-
# sleep 10
-
# t = ts[0]
-
# Process.kill("TERM", t.pid)
-
# p t.value #=> #<Process::Status: pid 911 SIGTERM (signal 15)>
-
# }
-
#
-
# # Convert pdf to ps and send it to a printer.
-
# # Collect error message of pdftops and lpr.
-
# pdf_file = "paper.pdf"
-
# printer = "printer-name"
-
# err_r, err_w = IO.pipe
-
# Open3.pipeline_start(["pdftops", pdf_file, "-"],
-
# ["lpr", "-P#{printer}"],
-
# :err=>err_w) {|ts|
-
# err_w.close
-
# p err_r.read # error messages of pdftops and lpr.
-
# }
-
#
-
1
def pipeline_start(*cmds, &block)
-
if Hash === cmds.last
-
opts = cmds.pop.dup
-
else
-
opts = {}
-
end
-
-
if block
-
pipeline_run(cmds, opts, [], [], &block)
-
else
-
ts, = pipeline_run(cmds, opts, [], [])
-
ts
-
end
-
end
-
1
module_function :pipeline_start
-
-
# Open3.pipeline starts a list of commands as a pipeline.
-
# It waits for the completion of the commands.
-
# No pipes are created for stdin of the first command and
-
# stdout of the last command.
-
#
-
# status_list = Open3.pipeline(cmd1, cmd2, ... [, opts])
-
#
-
# Each cmd is a string or an array.
-
# If it is an array, the elements are passed to Process.spawn.
-
#
-
# cmd:
-
# commandline command line string which is passed to a shell
-
# [env, commandline, opts] command line string which is passed to a shell
-
# [env, cmdname, arg1, ..., opts] command name and one or more arguments (no shell)
-
# [env, [cmdname, argv0], arg1, ..., opts] command name and arguments including argv[0] (no shell)
-
#
-
# Note that env and opts are optional, as Process.spawn.
-
#
-
# Example:
-
#
-
# fname = "/usr/share/man/man1/ruby.1.gz"
-
# p Open3.pipeline(["zcat", fname], "nroff -man", "less")
-
# #=> [#<Process::Status: pid 11817 exit 0>,
-
# # #<Process::Status: pid 11820 exit 0>,
-
# # #<Process::Status: pid 11828 exit 0>]
-
#
-
# fname = "/usr/share/man/man1/ls.1.gz"
-
# Open3.pipeline(["zcat", fname], "nroff -man", "colcrt")
-
#
-
# # convert PDF to PS and send to a printer by lpr
-
# pdf_file = "paper.pdf"
-
# printer = "printer-name"
-
# Open3.pipeline(["pdftops", pdf_file, "-"],
-
# ["lpr", "-P#{printer}"])
-
#
-
# # count lines
-
# Open3.pipeline("sort", "uniq -c", :in=>"names.txt", :out=>"count")
-
#
-
# # cyclic pipeline
-
# r,w = IO.pipe
-
# w.print "ibase=14\n10\n"
-
# Open3.pipeline("bc", "tee /dev/tty", :in=>r, :out=>w)
-
# #=> 14
-
# # 18
-
# # 22
-
# # 30
-
# # 42
-
# # 58
-
# # 78
-
# # 106
-
# # 202
-
#
-
1
def pipeline(*cmds)
-
if Hash === cmds.last
-
opts = cmds.pop.dup
-
else
-
opts = {}
-
end
-
-
pipeline_run(cmds, opts, [], []) {|ts|
-
ts.map(&:value)
-
}
-
end
-
1
module_function :pipeline
-
-
1
def pipeline_run(cmds, pipeline_opts, child_io, parent_io) # :nodoc:
-
if cmds.empty?
-
raise ArgumentError, "no commands"
-
end
-
-
opts_base = pipeline_opts.dup
-
opts_base.delete :in
-
opts_base.delete :out
-
-
wait_thrs = []
-
r = nil
-
cmds.each_with_index {|cmd, i|
-
cmd_opts = opts_base.dup
-
if String === cmd
-
cmd = [cmd]
-
else
-
cmd_opts.update cmd.pop if Hash === cmd.last
-
end
-
if i == 0
-
if !cmd_opts.include?(:in)
-
if pipeline_opts.include?(:in)
-
cmd_opts[:in] = pipeline_opts[:in]
-
end
-
end
-
else
-
cmd_opts[:in] = r
-
end
-
if i != cmds.length - 1
-
r2, w2 = IO.pipe
-
cmd_opts[:out] = w2
-
else
-
if !cmd_opts.include?(:out)
-
if pipeline_opts.include?(:out)
-
cmd_opts[:out] = pipeline_opts[:out]
-
end
-
end
-
end
-
pid = spawn(*cmd, cmd_opts)
-
wait_thrs << Process.detach(pid)
-
r&.close
-
w2&.close
-
r = r2
-
}
-
result = parent_io + [wait_thrs]
-
child_io.each(&:close)
-
if defined? yield
-
begin
-
return yield(*result)
-
ensure
-
parent_io.each(&:close)
-
wait_thrs.each(&:join)
-
end
-
end
-
result
-
end
-
1
module_function :pipeline_run
-
1
class << self
-
1
private :pipeline_run
-
end
-
-
end
-
# frozen_string_literal: false
-
=begin
-
= Info
-
'OpenSSL for Ruby 2' project
-
Copyright (C) 2002 Michal Rokos <m.rokos@sh.cvut.cz>
-
All rights reserved.
-
-
= Licence
-
This program is licensed under the same licence as Ruby.
-
(See the file 'LICENCE'.)
-
=end
-
-
1
require 'openssl.so'
-
-
1
require 'openssl/bn'
-
1
require 'openssl/pkey'
-
1
require 'openssl/cipher'
-
1
require 'openssl/config'
-
1
require 'openssl/digest'
-
1
require 'openssl/x509'
-
1
require 'openssl/ssl'
-
1
require 'openssl/pkcs5'
-
# frozen_string_literal: false
-
#--
-
#
-
# = Ruby-space definitions that completes C-space funcs for BN
-
#
-
# = Info
-
# 'OpenSSL for Ruby 2' project
-
# Copyright (C) 2002 Michal Rokos <m.rokos@sh.cvut.cz>
-
# All rights reserved.
-
#
-
# = Licence
-
# This program is licensed under the same licence as Ruby.
-
# (See the file 'LICENCE'.)
-
#++
-
-
1
module OpenSSL
-
1
class BN
-
1
include Comparable
-
-
1
def pretty_print(q)
-
q.object_group(self) {
-
q.text ' '
-
q.text to_i.to_s
-
}
-
end
-
end # BN
-
end # OpenSSL
-
-
##
-
#--
-
# Add double dispatch to Integer
-
#++
-
1
class Integer
-
# Casts an Integer as an OpenSSL::BN
-
#
-
# See `man bn` for more info.
-
1
def to_bn
-
OpenSSL::BN::new(self)
-
end
-
end # Integer
-
# coding: binary
-
# frozen_string_literal: false
-
#--
-
#= Info
-
# 'OpenSSL for Ruby 2' project
-
# Copyright (C) 2001 GOTOU YUUZOU <gotoyuzo@notwork.org>
-
# All rights reserved.
-
#
-
#= Licence
-
# This program is licensed under the same licence as Ruby.
-
# (See the file 'LICENCE'.)
-
#++
-
-
##
-
# OpenSSL IO buffering mix-in module.
-
#
-
# This module allows an OpenSSL::SSL::SSLSocket to behave like an IO.
-
#
-
# You typically won't use this module directly, you can see it implemented in
-
# OpenSSL::SSL::SSLSocket.
-
-
1
module OpenSSL::Buffering
-
1
include Enumerable
-
-
##
-
# The "sync mode" of the SSLSocket.
-
#
-
# See IO#sync for full details.
-
-
1
attr_accessor :sync
-
-
##
-
# Default size to read from or write to the SSLSocket for buffer operations.
-
-
1
BLOCK_SIZE = 1024*16
-
-
##
-
# Creates an instance of OpenSSL's buffering IO module.
-
-
1
def initialize(*)
-
super
-
@eof = false
-
@rbuffer = ""
-
@sync = @io.sync
-
end
-
-
#
-
# for reading.
-
#
-
1
private
-
-
##
-
# Fills the buffer from the underlying SSLSocket
-
-
1
def fill_rbuff
-
begin
-
@rbuffer << self.sysread(BLOCK_SIZE)
-
rescue Errno::EAGAIN
-
retry
-
rescue EOFError
-
@eof = true
-
end
-
end
-
-
##
-
# Consumes _size_ bytes from the buffer
-
-
1
def consume_rbuff(size=nil)
-
if @rbuffer.empty?
-
nil
-
else
-
size = @rbuffer.size unless size
-
ret = @rbuffer[0, size]
-
@rbuffer[0, size] = ""
-
ret
-
end
-
end
-
-
1
public
-
-
##
-
# Reads _size_ bytes from the stream. If _buf_ is provided it must
-
# reference a string which will receive the data.
-
#
-
# See IO#read for full details.
-
-
1
def read(size=nil, buf=nil)
-
if size == 0
-
if buf
-
buf.clear
-
return buf
-
else
-
return ""
-
end
-
end
-
until @eof
-
break if size && size <= @rbuffer.size
-
fill_rbuff
-
end
-
ret = consume_rbuff(size) || ""
-
if buf
-
buf.replace(ret)
-
ret = buf
-
end
-
(size && ret.empty?) ? nil : ret
-
end
-
-
##
-
# Reads at most _maxlen_ bytes from the stream. If _buf_ is provided it
-
# must reference a string which will receive the data.
-
#
-
# See IO#readpartial for full details.
-
-
1
def readpartial(maxlen, buf=nil)
-
if maxlen == 0
-
if buf
-
buf.clear
-
return buf
-
else
-
return ""
-
end
-
end
-
if @rbuffer.empty?
-
begin
-
return sysread(maxlen, buf)
-
rescue Errno::EAGAIN
-
retry
-
end
-
end
-
ret = consume_rbuff(maxlen)
-
if buf
-
buf.replace(ret)
-
ret = buf
-
end
-
ret
-
end
-
-
##
-
# Reads at most _maxlen_ bytes in the non-blocking manner.
-
#
-
# When no data can be read without blocking it raises
-
# OpenSSL::SSL::SSLError extended by IO::WaitReadable or IO::WaitWritable.
-
#
-
# IO::WaitReadable means SSL needs to read internally so read_nonblock
-
# should be called again when the underlying IO is readable.
-
#
-
# IO::WaitWritable means SSL needs to write internally so read_nonblock
-
# should be called again after the underlying IO is writable.
-
#
-
# OpenSSL::Buffering#read_nonblock needs two rescue clause as follows:
-
#
-
# # emulates blocking read (readpartial).
-
# begin
-
# result = ssl.read_nonblock(maxlen)
-
# rescue IO::WaitReadable
-
# IO.select([io])
-
# retry
-
# rescue IO::WaitWritable
-
# IO.select(nil, [io])
-
# retry
-
# end
-
#
-
# Note that one reason that read_nonblock writes to the underlying IO is
-
# when the peer requests a new TLS/SSL handshake. See openssl the FAQ for
-
# more details. http://www.openssl.org/support/faq.html
-
#
-
# By specifying a keyword argument _exception_ to +false+, you can indicate
-
# that read_nonblock should not raise an IO::Wait*able exception, but
-
# return the symbol +:wait_writable+ or +:wait_readable+ instead. At EOF,
-
# it will return +nil+ instead of raising EOFError.
-
-
1
def read_nonblock(maxlen, buf=nil, exception: true)
-
if maxlen == 0
-
if buf
-
buf.clear
-
return buf
-
else
-
return ""
-
end
-
end
-
if @rbuffer.empty?
-
return sysread_nonblock(maxlen, buf, exception: exception)
-
end
-
ret = consume_rbuff(maxlen)
-
if buf
-
buf.replace(ret)
-
ret = buf
-
end
-
ret
-
end
-
-
##
-
# Reads the next "line" from the stream. Lines are separated by _eol_. If
-
# _limit_ is provided the result will not be longer than the given number of
-
# bytes.
-
#
-
# _eol_ may be a String or Regexp.
-
#
-
# Unlike IO#gets the line read will not be assigned to +$_+.
-
#
-
# Unlike IO#gets the separator must be provided if a limit is provided.
-
-
1
def gets(eol=$/, limit=nil)
-
idx = @rbuffer.index(eol)
-
until @eof
-
break if idx
-
fill_rbuff
-
idx = @rbuffer.index(eol)
-
end
-
if eol.is_a?(Regexp)
-
size = idx ? idx+$&.size : nil
-
else
-
size = idx ? idx+eol.size : nil
-
end
-
if size && limit && limit >= 0
-
size = [size, limit].min
-
end
-
consume_rbuff(size)
-
end
-
-
##
-
# Executes the block for every line in the stream where lines are separated
-
# by _eol_.
-
#
-
# See also #gets
-
-
1
def each(eol=$/)
-
while line = self.gets(eol)
-
yield line
-
end
-
end
-
1
alias each_line each
-
-
##
-
# Reads lines from the stream which are separated by _eol_.
-
#
-
# See also #gets
-
-
1
def readlines(eol=$/)
-
ary = []
-
while line = self.gets(eol)
-
ary << line
-
end
-
ary
-
end
-
-
##
-
# Reads a line from the stream which is separated by _eol_.
-
#
-
# Raises EOFError if at end of file.
-
-
1
def readline(eol=$/)
-
raise EOFError if eof?
-
gets(eol)
-
end
-
-
##
-
# Reads one character from the stream. Returns nil if called at end of
-
# file.
-
-
1
def getc
-
read(1)
-
end
-
-
##
-
# Calls the given block once for each byte in the stream.
-
-
1
def each_byte # :yields: byte
-
while c = getc
-
yield(c.ord)
-
end
-
end
-
-
##
-
# Reads a one-character string from the stream. Raises an EOFError at end
-
# of file.
-
-
1
def readchar
-
raise EOFError if eof?
-
getc
-
end
-
-
##
-
# Pushes character _c_ back onto the stream such that a subsequent buffered
-
# character read will return it.
-
#
-
# Unlike IO#getc multiple bytes may be pushed back onto the stream.
-
#
-
# Has no effect on unbuffered reads (such as #sysread).
-
-
1
def ungetc(c)
-
@rbuffer[0,0] = c.chr
-
end
-
-
##
-
# Returns true if the stream is at file which means there is no more data to
-
# be read.
-
-
1
def eof?
-
fill_rbuff if !@eof && @rbuffer.empty?
-
@eof && @rbuffer.empty?
-
end
-
1
alias eof eof?
-
-
#
-
# for writing.
-
#
-
1
private
-
-
##
-
# Writes _s_ to the buffer. When the buffer is full or #sync is true the
-
# buffer is flushed to the underlying socket.
-
-
1
def do_write(s)
-
@wbuffer = "" unless defined? @wbuffer
-
@wbuffer << s
-
@wbuffer.force_encoding(Encoding::BINARY)
-
@sync ||= false
-
if @sync or @wbuffer.size > BLOCK_SIZE
-
until @wbuffer.empty?
-
begin
-
nwrote = syswrite(@wbuffer)
-
rescue Errno::EAGAIN
-
retry
-
end
-
@wbuffer[0, nwrote] = ""
-
end
-
end
-
end
-
-
1
public
-
-
##
-
# Writes _s_ to the stream. If the argument is not a String it will be
-
# converted using +.to_s+ method. Returns the number of bytes written.
-
-
1
def write(*s)
-
s.inject(0) do |written, str|
-
do_write(str)
-
written + str.bytesize
-
end
-
end
-
-
##
-
# Writes _s_ in the non-blocking manner.
-
#
-
# If there is buffered data, it is flushed first. This may block.
-
#
-
# write_nonblock returns number of bytes written to the SSL connection.
-
#
-
# When no data can be written without blocking it raises
-
# OpenSSL::SSL::SSLError extended by IO::WaitReadable or IO::WaitWritable.
-
#
-
# IO::WaitReadable means SSL needs to read internally so write_nonblock
-
# should be called again after the underlying IO is readable.
-
#
-
# IO::WaitWritable means SSL needs to write internally so write_nonblock
-
# should be called again after underlying IO is writable.
-
#
-
# So OpenSSL::Buffering#write_nonblock needs two rescue clause as follows.
-
#
-
# # emulates blocking write.
-
# begin
-
# result = ssl.write_nonblock(str)
-
# rescue IO::WaitReadable
-
# IO.select([io])
-
# retry
-
# rescue IO::WaitWritable
-
# IO.select(nil, [io])
-
# retry
-
# end
-
#
-
# Note that one reason that write_nonblock reads from the underlying IO
-
# is when the peer requests a new TLS/SSL handshake. See the openssl FAQ
-
# for more details. http://www.openssl.org/support/faq.html
-
#
-
# By specifying a keyword argument _exception_ to +false+, you can indicate
-
# that write_nonblock should not raise an IO::Wait*able exception, but
-
# return the symbol +:wait_writable+ or +:wait_readable+ instead.
-
-
1
def write_nonblock(s, exception: true)
-
flush
-
syswrite_nonblock(s, exception: exception)
-
end
-
-
##
-
# Writes _s_ to the stream. _s_ will be converted to a String using
-
# +.to_s+ method.
-
-
1
def <<(s)
-
do_write(s)
-
self
-
end
-
-
##
-
# Writes _args_ to the stream along with a record separator.
-
#
-
# See IO#puts for full details.
-
-
1
def puts(*args)
-
s = ""
-
if args.empty?
-
s << "\n"
-
end
-
args.each{|arg|
-
s << arg.to_s
-
s.sub!(/(?<!\n)\z/, "\n")
-
}
-
do_write(s)
-
nil
-
end
-
-
##
-
# Writes _args_ to the stream.
-
#
-
# See IO#print for full details.
-
-
1
def print(*args)
-
s = ""
-
args.each{ |arg| s << arg.to_s }
-
do_write(s)
-
nil
-
end
-
-
##
-
# Formats and writes to the stream converting parameters under control of
-
# the format string.
-
#
-
# See Kernel#sprintf for format string details.
-
-
1
def printf(s, *args)
-
do_write(s % args)
-
nil
-
end
-
-
##
-
# Flushes buffered data to the SSLSocket.
-
-
1
def flush
-
osync = @sync
-
@sync = true
-
do_write ""
-
return self
-
ensure
-
@sync = osync
-
end
-
-
##
-
# Closes the SSLSocket and flushes any unwritten data.
-
-
1
def close
-
flush rescue nil
-
sysclose
-
end
-
end
-
# frozen_string_literal: false
-
#--
-
# = Ruby-space predefined Cipher subclasses
-
#
-
# = Info
-
# 'OpenSSL for Ruby 2' project
-
# Copyright (C) 2002 Michal Rokos <m.rokos@sh.cvut.cz>
-
# All rights reserved.
-
#
-
# = Licence
-
# This program is licensed under the same licence as Ruby.
-
# (See the file 'LICENCE'.)
-
#++
-
-
1
module OpenSSL
-
1
class Cipher
-
1
%w(AES CAST5 BF DES IDEA RC2 RC4 RC5).each{|name|
-
8
klass = Class.new(Cipher){
-
8
define_method(:initialize){|*args|
-
cipher_name = args.inject(name){|n, arg| "#{n}-#{arg}" }
-
super(cipher_name.downcase)
-
}
-
}
-
8
const_set(name, klass)
-
}
-
-
1
%w(128 192 256).each{|keylen|
-
3
klass = Class.new(Cipher){
-
3
define_method(:initialize){|mode = "CBC"|
-
super("aes-#{keylen}-#{mode}".downcase)
-
}
-
}
-
3
const_set("AES#{keylen}", klass)
-
}
-
-
# call-seq:
-
# cipher.random_key -> key
-
#
-
# Generate a random key with OpenSSL::Random.random_bytes and sets it to
-
# the cipher, and returns it.
-
#
-
# You must call #encrypt or #decrypt before calling this method.
-
1
def random_key
-
str = OpenSSL::Random.random_bytes(self.key_len)
-
self.key = str
-
end
-
-
# call-seq:
-
# cipher.random_iv -> iv
-
#
-
# Generate a random IV with OpenSSL::Random.random_bytes and sets it to the
-
# cipher, and returns it.
-
#
-
# You must call #encrypt or #decrypt before calling this method.
-
1
def random_iv
-
str = OpenSSL::Random.random_bytes(self.iv_len)
-
self.iv = str
-
end
-
-
# Deprecated.
-
#
-
# This class is only provided for backwards compatibility.
-
# Use OpenSSL::Cipher.
-
1
class Cipher < Cipher; end
-
1
deprecate_constant :Cipher
-
end # Cipher
-
end # OpenSSL
-
# frozen_string_literal: false
-
=begin
-
= Ruby-space definitions that completes C-space funcs for Config
-
-
= Info
-
Copyright (C) 2010 Hiroshi Nakamura <nahi@ruby-lang.org>
-
-
= Licence
-
This program is licensed under the same licence as Ruby.
-
(See the file 'LICENCE'.)
-
-
=end
-
-
1
require 'stringio'
-
-
1
module OpenSSL
-
##
-
# = OpenSSL::Config
-
#
-
# Configuration for the openssl library.
-
#
-
# Many system's installation of openssl library will depend on your system
-
# configuration. See the value of OpenSSL::Config::DEFAULT_CONFIG_FILE for
-
# the location of the file for your host.
-
#
-
# See also http://www.openssl.org/docs/apps/config.html
-
1
class Config
-
1
include Enumerable
-
-
1
class << self
-
-
##
-
# Parses a given _string_ as a blob that contains configuration for
-
# OpenSSL.
-
#
-
# If the source of the IO is a file, then consider using #parse_config.
-
1
def parse(string)
-
c = new()
-
parse_config(StringIO.new(string)).each do |section, hash|
-
c[section] = hash
-
end
-
c
-
end
-
-
##
-
# load is an alias to ::new
-
1
alias load new
-
-
##
-
# Parses the configuration data read from _io_, see also #parse.
-
#
-
# Raises a ConfigError on invalid configuration data.
-
1
def parse_config(io)
-
begin
-
parse_config_lines(io)
-
rescue ConfigError => e
-
e.message.replace("error in line #{io.lineno}: " + e.message)
-
raise
-
end
-
end
-
-
1
def get_key_string(data, section, key) # :nodoc:
-
if v = data[section] && data[section][key]
-
return v
-
elsif section == 'ENV'
-
if v = ENV[key]
-
return v
-
end
-
end
-
if v = data['default'] && data['default'][key]
-
return v
-
end
-
end
-
-
1
private
-
-
1
def parse_config_lines(io)
-
section = 'default'
-
data = {section => {}}
-
io_stack = [io]
-
while definition = get_definition(io_stack)
-
definition = clear_comments(definition)
-
next if definition.empty?
-
case definition
-
when /\A\[/
-
if /\[([^\]]*)\]/ =~ definition
-
section = $1.strip
-
data[section] ||= {}
-
else
-
raise ConfigError, "missing close square bracket"
-
end
-
when /\A\.include (\s*=\s*)?(.+)\z/
-
path = $2
-
if File.directory?(path)
-
files = Dir.glob(File.join(path, "*.{cnf,conf}"), File::FNM_EXTGLOB)
-
else
-
files = [path]
-
end
-
-
files.each do |filename|
-
begin
-
io_stack << StringIO.new(File.read(filename))
-
rescue
-
raise ConfigError, "could not include file '%s'" % filename
-
end
-
end
-
when /\A([^:\s]*)(?:::([^:\s]*))?\s*=(.*)\z/
-
if $2
-
section = $1
-
key = $2
-
else
-
key = $1
-
end
-
value = unescape_value(data, section, $3)
-
(data[section] ||= {})[key] = value.strip
-
else
-
raise ConfigError, "missing equal sign"
-
end
-
end
-
data
-
end
-
-
# escape with backslash
-
1
QUOTE_REGEXP_SQ = /\A([^'\\]*(?:\\.[^'\\]*)*)'/
-
# escape with backslash and doubled dq
-
1
QUOTE_REGEXP_DQ = /\A([^"\\]*(?:""[^"\\]*|\\.[^"\\]*)*)"/
-
# escaped char map
-
ESCAPE_MAP = {
-
1
"r" => "\r",
-
"n" => "\n",
-
"b" => "\b",
-
"t" => "\t",
-
}
-
-
1
def unescape_value(data, section, value)
-
scanned = []
-
while m = value.match(/['"\\$]/)
-
scanned << m.pre_match
-
c = m[0]
-
value = m.post_match
-
case c
-
when "'"
-
if m = value.match(QUOTE_REGEXP_SQ)
-
scanned << m[1].gsub(/\\(.)/, '\\1')
-
value = m.post_match
-
else
-
break
-
end
-
when '"'
-
if m = value.match(QUOTE_REGEXP_DQ)
-
scanned << m[1].gsub(/""/, '').gsub(/\\(.)/, '\\1')
-
value = m.post_match
-
else
-
break
-
end
-
when "\\"
-
c = value.slice!(0, 1)
-
scanned << (ESCAPE_MAP[c] || c)
-
when "$"
-
ref, value = extract_reference(value)
-
refsec = section
-
if ref.index('::')
-
refsec, ref = ref.split('::', 2)
-
end
-
if v = get_key_string(data, refsec, ref)
-
scanned << v
-
else
-
raise ConfigError, "variable has no value"
-
end
-
else
-
raise 'must not reaced'
-
end
-
end
-
scanned << value
-
scanned.join
-
end
-
-
1
def extract_reference(value)
-
rest = ''
-
if m = value.match(/\(([^)]*)\)|\{([^}]*)\}/)
-
value = m[1] || m[2]
-
rest = m.post_match
-
elsif [?(, ?{].include?(value[0])
-
raise ConfigError, "no close brace"
-
end
-
if m = value.match(/[a-zA-Z0-9_]*(?:::[a-zA-Z0-9_]*)?/)
-
return m[0], m.post_match + rest
-
else
-
raise
-
end
-
end
-
-
1
def clear_comments(line)
-
# FCOMMENT
-
if m = line.match(/\A([\t\n\f ]*);.*\z/)
-
return m[1]
-
end
-
# COMMENT
-
scanned = []
-
while m = line.match(/[#'"\\]/)
-
scanned << m.pre_match
-
c = m[0]
-
line = m.post_match
-
case c
-
when '#'
-
line = nil
-
break
-
when "'", '"'
-
regexp = (c == "'") ? QUOTE_REGEXP_SQ : QUOTE_REGEXP_DQ
-
scanned << c
-
if m = line.match(regexp)
-
scanned << m[0]
-
line = m.post_match
-
else
-
scanned << line
-
line = nil
-
break
-
end
-
when "\\"
-
scanned << c
-
scanned << line.slice!(0, 1)
-
else
-
raise 'must not reaced'
-
end
-
end
-
scanned << line
-
scanned.join
-
end
-
-
1
def get_definition(io_stack)
-
if line = get_line(io_stack)
-
while /[^\\]\\\z/ =~ line
-
if extra = get_line(io_stack)
-
line += extra
-
else
-
break
-
end
-
end
-
return line.strip
-
end
-
end
-
-
1
def get_line(io_stack)
-
while io = io_stack.last
-
if line = io.gets
-
return line.gsub(/[\r\n]*/, '')
-
end
-
io_stack.pop
-
end
-
end
-
end
-
-
##
-
# Creates an instance of OpenSSL's configuration class.
-
#
-
# This can be used in contexts like OpenSSL::X509::ExtensionFactory.config=
-
#
-
# If the optional _filename_ parameter is provided, then it is read in and
-
# parsed via #parse_config.
-
#
-
# This can raise IO exceptions based on the access, or availability of the
-
# file. A ConfigError exception may be raised depending on the validity of
-
# the data being configured.
-
#
-
1
def initialize(filename = nil)
-
@data = {}
-
if filename
-
File.open(filename.to_s) do |file|
-
Config.parse_config(file).each do |section, hash|
-
self[section] = hash
-
end
-
end
-
end
-
end
-
-
##
-
# Gets the value of _key_ from the given _section_
-
#
-
# Given the following configurating file being loaded:
-
#
-
# config = OpenSSL::Config.load('foo.cnf')
-
# #=> #<OpenSSL::Config sections=["default"]>
-
# puts config.to_s
-
# #=> [ default ]
-
# # foo=bar
-
#
-
# You can get a specific value from the config if you know the _section_
-
# and _key_ like so:
-
#
-
# config.get_value('default','foo')
-
# #=> "bar"
-
#
-
1
def get_value(section, key)
-
if section.nil?
-
raise TypeError.new('nil not allowed')
-
end
-
section = 'default' if section.empty?
-
get_key_string(section, key)
-
end
-
-
##
-
#
-
# *Deprecated*
-
#
-
# Use #get_value instead
-
1
def value(arg1, arg2 = nil) # :nodoc:
-
warn('Config#value is deprecated; use Config#get_value')
-
if arg2.nil?
-
section, key = 'default', arg1
-
else
-
section, key = arg1, arg2
-
end
-
section ||= 'default'
-
section = 'default' if section.empty?
-
get_key_string(section, key)
-
end
-
-
##
-
# Set the target _key_ with a given _value_ under a specific _section_.
-
#
-
# Given the following configurating file being loaded:
-
#
-
# config = OpenSSL::Config.load('foo.cnf')
-
# #=> #<OpenSSL::Config sections=["default"]>
-
# puts config.to_s
-
# #=> [ default ]
-
# # foo=bar
-
#
-
# You can set the value of _foo_ under the _default_ section to a new
-
# value:
-
#
-
# config.add_value('default', 'foo', 'buzz')
-
# #=> "buzz"
-
# puts config.to_s
-
# #=> [ default ]
-
# # foo=buzz
-
#
-
1
def add_value(section, key, value)
-
check_modify
-
(@data[section] ||= {})[key] = value
-
end
-
-
##
-
# Get a specific _section_ from the current configuration
-
#
-
# Given the following configurating file being loaded:
-
#
-
# config = OpenSSL::Config.load('foo.cnf')
-
# #=> #<OpenSSL::Config sections=["default"]>
-
# puts config.to_s
-
# #=> [ default ]
-
# # foo=bar
-
#
-
# You can get a hash of the specific section like so:
-
#
-
# config['default']
-
# #=> {"foo"=>"bar"}
-
#
-
1
def [](section)
-
@data[section] || {}
-
end
-
-
##
-
# Deprecated
-
#
-
# Use #[] instead
-
1
def section(name) # :nodoc:
-
warn('Config#section is deprecated; use Config#[]')
-
@data[name] || {}
-
end
-
-
##
-
# Sets a specific _section_ name with a Hash _pairs_.
-
#
-
# Given the following configuration being created:
-
#
-
# config = OpenSSL::Config.new
-
# #=> #<OpenSSL::Config sections=[]>
-
# config['default'] = {"foo"=>"bar","baz"=>"buz"}
-
# #=> {"foo"=>"bar", "baz"=>"buz"}
-
# puts config.to_s
-
# #=> [ default ]
-
# # foo=bar
-
# # baz=buz
-
#
-
# It's important to note that this will essentially merge any of the keys
-
# in _pairs_ with the existing _section_. For example:
-
#
-
# config['default']
-
# #=> {"foo"=>"bar", "baz"=>"buz"}
-
# config['default'] = {"foo" => "changed"}
-
# #=> {"foo"=>"changed"}
-
# config['default']
-
# #=> {"foo"=>"changed", "baz"=>"buz"}
-
#
-
1
def []=(section, pairs)
-
check_modify
-
@data[section] ||= {}
-
pairs.each do |key, value|
-
self.add_value(section, key, value)
-
end
-
end
-
-
##
-
# Get the names of all sections in the current configuration
-
1
def sections
-
@data.keys
-
end
-
-
##
-
# Get the parsable form of the current configuration
-
#
-
# Given the following configuration being created:
-
#
-
# config = OpenSSL::Config.new
-
# #=> #<OpenSSL::Config sections=[]>
-
# config['default'] = {"foo"=>"bar","baz"=>"buz"}
-
# #=> {"foo"=>"bar", "baz"=>"buz"}
-
# puts config.to_s
-
# #=> [ default ]
-
# # foo=bar
-
# # baz=buz
-
#
-
# You can parse get the serialized configuration using #to_s and then parse
-
# it later:
-
#
-
# serialized_config = config.to_s
-
# # much later...
-
# new_config = OpenSSL::Config.parse(serialized_config)
-
# #=> #<OpenSSL::Config sections=["default"]>
-
# puts new_config
-
# #=> [ default ]
-
# foo=bar
-
# baz=buz
-
#
-
1
def to_s
-
ary = []
-
@data.keys.sort.each do |section|
-
ary << "[ #{section} ]\n"
-
@data[section].keys.each do |key|
-
ary << "#{key}=#{@data[section][key]}\n"
-
end
-
ary << "\n"
-
end
-
ary.join
-
end
-
-
##
-
# For a block.
-
#
-
# Receive the section and its pairs for the current configuration.
-
#
-
# config.each do |section, key, value|
-
# # ...
-
# end
-
#
-
1
def each
-
@data.each do |section, hash|
-
hash.each do |key, value|
-
yield [section, key, value]
-
end
-
end
-
end
-
-
##
-
# String representation of this configuration object, including the class
-
# name and its sections.
-
1
def inspect
-
"#<#{self.class.name} sections=#{sections.inspect}>"
-
end
-
-
1
protected
-
-
1
def data # :nodoc:
-
@data
-
end
-
-
1
private
-
-
1
def initialize_copy(other)
-
@data = other.data.dup
-
end
-
-
1
def check_modify
-
raise TypeError.new("Insecure: can't modify OpenSSL config") if frozen?
-
end
-
-
1
def get_key_string(section, key)
-
Config.get_key_string(@data, section, key)
-
end
-
end
-
end
-
# frozen_string_literal: false
-
#--
-
# = Ruby-space predefined Digest subclasses
-
#
-
# = Info
-
# 'OpenSSL for Ruby 2' project
-
# Copyright (C) 2002 Michal Rokos <m.rokos@sh.cvut.cz>
-
# All rights reserved.
-
#
-
# = Licence
-
# This program is licensed under the same licence as Ruby.
-
# (See the file 'LICENCE'.)
-
#++
-
-
1
module OpenSSL
-
1
class Digest
-
-
1
alg = %w(MD2 MD4 MD5 MDC2 RIPEMD160 SHA1 SHA224 SHA256 SHA384 SHA512)
-
1
if OPENSSL_VERSION_NUMBER < 0x10100000
-
alg += %w(DSS DSS1 SHA)
-
end
-
-
# Return the hash value computed with _name_ Digest. _name_ is either the
-
# long name or short name of a supported digest algorithm.
-
#
-
# === Examples
-
#
-
# OpenSSL::Digest.digest("SHA256", "abc")
-
#
-
# which is equivalent to:
-
#
-
# OpenSSL::Digest::SHA256.digest("abc")
-
-
1
def self.digest(name, data)
-
super(data, name)
-
end
-
-
1
alg.each{|name|
-
10
klass = Class.new(self) {
-
11
define_method(:initialize, ->(data = nil) {super(name, data)})
-
}
-
20
singleton = (class << klass; self; end)
-
10
singleton.class_eval{
-
10
define_method(:digest){|data| new.digest(data) }
-
10
define_method(:hexdigest){|data| new.hexdigest(data) }
-
}
-
10
const_set(name, klass)
-
}
-
-
# Deprecated.
-
#
-
# This class is only provided for backwards compatibility.
-
# Use OpenSSL::Digest instead.
-
1
class Digest < Digest; end # :nodoc:
-
1
deprecate_constant :Digest
-
-
end # Digest
-
-
# Returns a Digest subclass by _name_
-
#
-
# require 'openssl'
-
#
-
# OpenSSL::Digest("MD5")
-
# # => OpenSSL::Digest::MD5
-
#
-
# Digest("Foo")
-
# # => NameError: wrong constant name Foo
-
-
1
def Digest(name)
-
OpenSSL::Digest.const_get(name)
-
end
-
-
1
module_function :Digest
-
-
end # OpenSSL
-
# frozen_string_literal: false
-
#--
-
# Ruby/OpenSSL Project
-
# Copyright (C) 2017 Ruby/OpenSSL Project Authors
-
#++
-
-
1
module OpenSSL
-
1
module PKCS5
-
1
module_function
-
-
# OpenSSL::PKCS5.pbkdf2_hmac has been renamed to OpenSSL::KDF.pbkdf2_hmac.
-
# This method is provided for backwards compatibility.
-
1
def pbkdf2_hmac(pass, salt, iter, keylen, digest)
-
OpenSSL::KDF.pbkdf2_hmac(pass, salt: salt, iterations: iter,
-
length: keylen, hash: digest)
-
end
-
-
1
def pbkdf2_hmac_sha1(pass, salt, iter, keylen)
-
pbkdf2_hmac(pass, salt, iter, keylen, "sha1")
-
end
-
end
-
end
-
# frozen_string_literal: false
-
#--
-
# Ruby/OpenSSL Project
-
# Copyright (C) 2017 Ruby/OpenSSL Project Authors
-
#++
-
-
1
module OpenSSL::PKey
-
1
if defined?(EC)
-
1
class EC::Point
-
# :call-seq:
-
# point.to_bn([conversion_form]) -> OpenSSL::BN
-
#
-
# Returns the octet string representation of the EC point as an instance of
-
# OpenSSL::BN.
-
#
-
# If _conversion_form_ is not given, the _point_conversion_form_ attribute
-
# set to the group is used.
-
#
-
# See #to_octet_string for more information.
-
1
def to_bn(conversion_form = group.point_conversion_form)
-
OpenSSL::BN.new(to_octet_string(conversion_form), 2)
-
end
-
end
-
end
-
end
-
# frozen_string_literal: false
-
=begin
-
= Info
-
'OpenSSL for Ruby 2' project
-
Copyright (C) 2001 GOTOU YUUZOU <gotoyuzo@notwork.org>
-
All rights reserved.
-
-
= Licence
-
This program is licensed under the same licence as Ruby.
-
(See the file 'LICENCE'.)
-
=end
-
-
1
require "openssl/buffering"
-
1
require "io/nonblock"
-
1
require "ipaddr"
-
-
1
module OpenSSL
-
1
module SSL
-
1
class SSLContext
-
DEFAULT_PARAMS = { # :nodoc:
-
1
:min_version => OpenSSL::SSL::TLS1_VERSION,
-
:verify_mode => OpenSSL::SSL::VERIFY_PEER,
-
:verify_hostname => true,
-
:options => -> {
-
1
opts = OpenSSL::SSL::OP_ALL
-
1
opts &= ~OpenSSL::SSL::OP_DONT_INSERT_EMPTY_FRAGMENTS
-
1
opts |= OpenSSL::SSL::OP_NO_COMPRESSION
-
1
opts
-
}.call
-
}
-
-
1
if defined?(OpenSSL::PKey::DH)
-
1
DEFAULT_2048 = OpenSSL::PKey::DH.new <<-_end_of_pem_
-
-----BEGIN DH PARAMETERS-----
-
MIIBCAKCAQEA7E6kBrYiyvmKAMzQ7i8WvwVk9Y/+f8S7sCTN712KkK3cqd1jhJDY
-
JbrYeNV3kUIKhPxWHhObHKpD1R84UpL+s2b55+iMd6GmL7OYmNIT/FccKhTcveab
-
VBmZT86BZKYyf45hUF9FOuUM9xPzuK3Vd8oJQvfYMCd7LPC0taAEljQLR4Edf8E6
-
YoaOffgTf5qxiwkjnlVZQc3whgnEt9FpVMvQ9eknyeGB5KHfayAc3+hUAvI3/Cr3
-
1bNveX5wInh5GDx1FGhKBZ+s1H+aedudCm7sCgRwv8lKWYGiHzObSma8A86KG+MD
-
7Lo5JquQ3DlBodj3IDyPrxIv96lvRPFtAwIBAg==
-
-----END DH PARAMETERS-----
-
_end_of_pem_
-
1
private_constant :DEFAULT_2048
-
-
1
DEFAULT_TMP_DH_CALLBACK = lambda { |ctx, is_export, keylen| # :nodoc:
-
warn "using default DH parameters." if $VERBOSE
-
DEFAULT_2048
-
}
-
end
-
-
1
if !(OpenSSL::OPENSSL_VERSION.start_with?("OpenSSL") &&
-
OpenSSL::OPENSSL_VERSION_NUMBER >= 0x10100000)
-
DEFAULT_PARAMS.merge!(
-
ciphers: %w{
-
ECDHE-ECDSA-AES128-GCM-SHA256
-
ECDHE-RSA-AES128-GCM-SHA256
-
ECDHE-ECDSA-AES256-GCM-SHA384
-
ECDHE-RSA-AES256-GCM-SHA384
-
DHE-RSA-AES128-GCM-SHA256
-
DHE-DSS-AES128-GCM-SHA256
-
DHE-RSA-AES256-GCM-SHA384
-
DHE-DSS-AES256-GCM-SHA384
-
ECDHE-ECDSA-AES128-SHA256
-
ECDHE-RSA-AES128-SHA256
-
ECDHE-ECDSA-AES128-SHA
-
ECDHE-RSA-AES128-SHA
-
ECDHE-ECDSA-AES256-SHA384
-
ECDHE-RSA-AES256-SHA384
-
ECDHE-ECDSA-AES256-SHA
-
ECDHE-RSA-AES256-SHA
-
DHE-RSA-AES128-SHA256
-
DHE-RSA-AES256-SHA256
-
DHE-RSA-AES128-SHA
-
DHE-RSA-AES256-SHA
-
DHE-DSS-AES128-SHA256
-
DHE-DSS-AES256-SHA256
-
DHE-DSS-AES128-SHA
-
DHE-DSS-AES256-SHA
-
AES128-GCM-SHA256
-
AES256-GCM-SHA384
-
AES128-SHA256
-
AES256-SHA256
-
AES128-SHA
-
AES256-SHA
-
}.join(":"),
-
)
-
end
-
-
1
DEFAULT_CERT_STORE = OpenSSL::X509::Store.new # :nodoc:
-
1
DEFAULT_CERT_STORE.set_default_paths
-
1
DEFAULT_CERT_STORE.flags = OpenSSL::X509::V_FLAG_CRL_CHECK_ALL
-
-
# A callback invoked when DH parameters are required.
-
#
-
# The callback is invoked with the Session for the key exchange, an
-
# flag indicating the use of an export cipher and the keylength
-
# required.
-
#
-
# The callback must return an OpenSSL::PKey::DH instance of the correct
-
# key length.
-
-
1
attr_accessor :tmp_dh_callback
-
-
# A callback invoked at connect time to distinguish between multiple
-
# server names.
-
#
-
# The callback is invoked with an SSLSocket and a server name. The
-
# callback must return an SSLContext for the server name or nil.
-
1
attr_accessor :servername_cb
-
-
# call-seq:
-
# SSLContext.new -> ctx
-
# SSLContext.new(:TLSv1) -> ctx
-
# SSLContext.new("SSLv23") -> ctx
-
#
-
# Creates a new SSL context.
-
#
-
# If an argument is given, #ssl_version= is called with the value. Note
-
# that this form is deprecated. New applications should use #min_version=
-
# and #max_version= as necessary.
-
1
def initialize(version = nil)
-
self.options |= OpenSSL::SSL::OP_ALL
-
self.ssl_version = version if version
-
end
-
-
##
-
# call-seq:
-
# ctx.set_params(params = {}) -> params
-
#
-
# Sets saner defaults optimized for the use with HTTP-like protocols.
-
#
-
# If a Hash _params_ is given, the parameters are overridden with it.
-
# The keys in _params_ must be assignment methods on SSLContext.
-
#
-
# If the verify_mode is not VERIFY_NONE and ca_file, ca_path and
-
# cert_store are not set then the system default certificate store is
-
# used.
-
1
def set_params(params={})
-
params = DEFAULT_PARAMS.merge(params)
-
self.options = params.delete(:options) # set before min_version/max_version
-
params.each{|name, value| self.__send__("#{name}=", value) }
-
if self.verify_mode != OpenSSL::SSL::VERIFY_NONE
-
unless self.ca_file or self.ca_path or self.cert_store
-
self.cert_store = DEFAULT_CERT_STORE
-
end
-
end
-
return params
-
end
-
-
# call-seq:
-
# ctx.min_version = OpenSSL::SSL::TLS1_2_VERSION
-
# ctx.min_version = :TLS1_2
-
# ctx.min_version = nil
-
#
-
# Sets the lower bound on the supported SSL/TLS protocol version. The
-
# version may be specified by an integer constant named
-
# OpenSSL::SSL::*_VERSION, a Symbol, or +nil+ which means "any version".
-
#
-
# Be careful that you don't overwrite OpenSSL::SSL::OP_NO_{SSL,TLS}v*
-
# options by #options= once you have called #min_version= or
-
# #max_version=.
-
#
-
# === Example
-
# ctx = OpenSSL::SSL::SSLContext.new
-
# ctx.min_version = OpenSSL::SSL::TLS1_1_VERSION
-
# ctx.max_version = OpenSSL::SSL::TLS1_2_VERSION
-
#
-
# sock = OpenSSL::SSL::SSLSocket.new(tcp_sock, ctx)
-
# sock.connect # Initiates a connection using either TLS 1.1 or TLS 1.2
-
1
def min_version=(version)
-
set_minmax_proto_version(version, @max_proto_version ||= nil)
-
@min_proto_version = version
-
end
-
-
# call-seq:
-
# ctx.max_version = OpenSSL::SSL::TLS1_2_VERSION
-
# ctx.max_version = :TLS1_2
-
# ctx.max_version = nil
-
#
-
# Sets the upper bound of the supported SSL/TLS protocol version. See
-
# #min_version= for the possible values.
-
1
def max_version=(version)
-
set_minmax_proto_version(@min_proto_version ||= nil, version)
-
@max_proto_version = version
-
end
-
-
# call-seq:
-
# ctx.ssl_version = :TLSv1
-
# ctx.ssl_version = "SSLv23"
-
#
-
# Sets the SSL/TLS protocol version for the context. This forces
-
# connections to use only the specified protocol version. This is
-
# deprecated and only provided for backwards compatibility. Use
-
# #min_version= and #max_version= instead.
-
#
-
# === History
-
# As the name hints, this used to call the SSL_CTX_set_ssl_version()
-
# function which sets the SSL method used for connections created from
-
# the context. As of Ruby/OpenSSL 2.1, this accessor method is
-
# implemented to call #min_version= and #max_version= instead.
-
1
def ssl_version=(meth)
-
meth = meth.to_s if meth.is_a?(Symbol)
-
if /(?<type>_client|_server)\z/ =~ meth
-
meth = $`
-
if $VERBOSE
-
warn "#{caller(1, 1)[0]}: method type #{type.inspect} is ignored"
-
end
-
end
-
version = METHODS_MAP[meth.intern] or
-
raise ArgumentError, "unknown SSL method `%s'" % meth
-
set_minmax_proto_version(version, version)
-
@min_proto_version = @max_proto_version = version
-
end
-
-
METHODS_MAP = {
-
1
SSLv23: 0,
-
SSLv2: OpenSSL::SSL::SSL2_VERSION,
-
SSLv3: OpenSSL::SSL::SSL3_VERSION,
-
TLSv1: OpenSSL::SSL::TLS1_VERSION,
-
TLSv1_1: OpenSSL::SSL::TLS1_1_VERSION,
-
TLSv1_2: OpenSSL::SSL::TLS1_2_VERSION,
-
}.freeze
-
1
private_constant :METHODS_MAP
-
-
# The list of available SSL/TLS methods. This constant is only provided
-
# for backwards compatibility.
-
1
METHODS = METHODS_MAP.flat_map { |name,|
-
6
[name, :"#{name}_client", :"#{name}_server"]
-
}.freeze
-
1
deprecate_constant :METHODS
-
end
-
-
1
module SocketForwarder
-
1
def addr
-
to_io.addr
-
end
-
-
1
def peeraddr
-
to_io.peeraddr
-
end
-
-
1
def setsockopt(level, optname, optval)
-
to_io.setsockopt(level, optname, optval)
-
end
-
-
1
def getsockopt(level, optname)
-
to_io.getsockopt(level, optname)
-
end
-
-
1
def fcntl(*args)
-
to_io.fcntl(*args)
-
end
-
-
1
def closed?
-
to_io.closed?
-
end
-
-
1
def do_not_reverse_lookup=(flag)
-
to_io.do_not_reverse_lookup = flag
-
end
-
end
-
-
1
def verify_certificate_identity(cert, hostname)
-
should_verify_common_name = true
-
cert.extensions.each{|ext|
-
next if ext.oid != "subjectAltName"
-
ostr = OpenSSL::ASN1.decode(ext.to_der).value.last
-
sequence = OpenSSL::ASN1.decode(ostr.value)
-
sequence.value.each{|san|
-
case san.tag
-
when 2 # dNSName in GeneralName (RFC5280)
-
should_verify_common_name = false
-
return true if verify_hostname(hostname, san.value)
-
when 7 # iPAddress in GeneralName (RFC5280)
-
should_verify_common_name = false
-
if san.value.size == 4 || san.value.size == 16
-
begin
-
return true if san.value == IPAddr.new(hostname).hton
-
rescue IPAddr::InvalidAddressError
-
end
-
end
-
end
-
}
-
}
-
if should_verify_common_name
-
cert.subject.to_a.each{|oid, value|
-
if oid == "CN"
-
return true if verify_hostname(hostname, value)
-
end
-
}
-
end
-
return false
-
end
-
1
module_function :verify_certificate_identity
-
-
1
def verify_hostname(hostname, san) # :nodoc:
-
# RFC 5280, IA5String is limited to the set of ASCII characters
-
return false unless san.ascii_only?
-
return false unless hostname.ascii_only?
-
-
# See RFC 6125, section 6.4.1
-
# Matching is case-insensitive.
-
san_parts = san.downcase.split(".")
-
-
# TODO: this behavior should probably be more strict
-
return san == hostname if san_parts.size < 2
-
-
# Matching is case-insensitive.
-
host_parts = hostname.downcase.split(".")
-
-
# RFC 6125, section 6.4.3, subitem 2.
-
# If the wildcard character is the only character of the left-most
-
# label in the presented identifier, the client SHOULD NOT compare
-
# against anything but the left-most label of the reference
-
# identifier (e.g., *.example.com would match foo.example.com but
-
# not bar.foo.example.com or example.com).
-
return false unless san_parts.size == host_parts.size
-
-
# RFC 6125, section 6.4.3, subitem 1.
-
# The client SHOULD NOT attempt to match a presented identifier in
-
# which the wildcard character comprises a label other than the
-
# left-most label (e.g., do not match bar.*.example.net).
-
return false unless verify_wildcard(host_parts.shift, san_parts.shift)
-
-
san_parts.join(".") == host_parts.join(".")
-
end
-
1
module_function :verify_hostname
-
-
1
def verify_wildcard(domain_component, san_component) # :nodoc:
-
parts = san_component.split("*", -1)
-
-
return false if parts.size > 2
-
return san_component == domain_component if parts.size == 1
-
-
# RFC 6125, section 6.4.3, subitem 3.
-
# The client SHOULD NOT attempt to match a presented identifier
-
# where the wildcard character is embedded within an A-label or
-
# U-label of an internationalized domain name.
-
return false if domain_component.start_with?("xn--") && san_component != "*"
-
-
parts[0].length + parts[1].length < domain_component.length &&
-
domain_component.start_with?(parts[0]) &&
-
domain_component.end_with?(parts[1])
-
end
-
1
module_function :verify_wildcard
-
-
1
class SSLSocket
-
1
include Buffering
-
1
include SocketForwarder
-
-
1
attr_reader :hostname
-
-
# The underlying IO object.
-
1
attr_reader :io
-
1
alias :to_io :io
-
-
# The SSLContext object used in this connection.
-
1
attr_reader :context
-
-
# Whether to close the underlying socket as well, when the SSL/TLS
-
# connection is shut down. This defaults to +false+.
-
1
attr_accessor :sync_close
-
-
# call-seq:
-
# ssl.sysclose => nil
-
#
-
# Sends "close notify" to the peer and tries to shut down the SSL
-
# connection gracefully.
-
#
-
# If sync_close is set to +true+, the underlying IO is also closed.
-
1
def sysclose
-
return if closed?
-
stop
-
io.close if sync_close
-
end
-
-
# call-seq:
-
# ssl.post_connection_check(hostname) -> true
-
#
-
# Perform hostname verification following RFC 6125.
-
#
-
# This method MUST be called after calling #connect to ensure that the
-
# hostname of a remote peer has been verified.
-
1
def post_connection_check(hostname)
-
if peer_cert.nil?
-
msg = "Peer verification enabled, but no certificate received."
-
if using_anon_cipher?
-
msg += " Anonymous cipher suite #{cipher[0]} was negotiated. " \
-
"Anonymous suites must be disabled to use peer verification."
-
end
-
raise SSLError, msg
-
end
-
-
unless OpenSSL::SSL.verify_certificate_identity(peer_cert, hostname)
-
raise SSLError, "hostname \"#{hostname}\" does not match the server certificate"
-
end
-
return true
-
end
-
-
# call-seq:
-
# ssl.session -> aSession
-
#
-
# Returns the SSLSession object currently used, or nil if the session is
-
# not established.
-
1
def session
-
SSL::Session.new(self)
-
rescue SSL::Session::SessionError
-
nil
-
end
-
-
1
private
-
-
1
def using_anon_cipher?
-
ctx = OpenSSL::SSL::SSLContext.new
-
ctx.ciphers = "aNULL"
-
ctx.ciphers.include?(cipher)
-
end
-
-
1
def client_cert_cb
-
@context.client_cert_cb
-
end
-
-
1
def tmp_dh_callback
-
@context.tmp_dh_callback || OpenSSL::SSL::SSLContext::DEFAULT_TMP_DH_CALLBACK
-
end
-
-
1
def tmp_ecdh_callback
-
@context.tmp_ecdh_callback
-
end
-
-
1
def session_new_cb
-
@context.session_new_cb
-
end
-
-
1
def session_get_cb
-
@context.session_get_cb
-
end
-
end
-
-
##
-
# SSLServer represents a TCP/IP server socket with Secure Sockets Layer.
-
1
class SSLServer
-
1
include SocketForwarder
-
# When true then #accept works exactly the same as TCPServer#accept
-
1
attr_accessor :start_immediately
-
-
# Creates a new instance of SSLServer.
-
# * _srv_ is an instance of TCPServer.
-
# * _ctx_ is an instance of OpenSSL::SSL::SSLContext.
-
1
def initialize(svr, ctx)
-
@svr = svr
-
@ctx = ctx
-
unless ctx.session_id_context
-
# see #6137 - session id may not exceed 32 bytes
-
prng = ::Random.new($0.hash)
-
session_id = prng.bytes(16).unpack('H*')[0]
-
@ctx.session_id_context = session_id
-
end
-
@start_immediately = true
-
end
-
-
# Returns the TCPServer passed to the SSLServer when initialized.
-
1
def to_io
-
@svr
-
end
-
-
# See TCPServer#listen for details.
-
1
def listen(backlog=5)
-
@svr.listen(backlog)
-
end
-
-
# See BasicSocket#shutdown for details.
-
1
def shutdown(how=Socket::SHUT_RDWR)
-
@svr.shutdown(how)
-
end
-
-
# Works similar to TCPServer#accept.
-
1
def accept
-
# Socket#accept returns [socket, addrinfo].
-
# TCPServer#accept returns a socket.
-
# The following comma strips addrinfo.
-
sock, = @svr.accept
-
begin
-
ssl = OpenSSL::SSL::SSLSocket.new(sock, @ctx)
-
ssl.sync_close = true
-
ssl.accept if @start_immediately
-
ssl
-
rescue Exception => ex
-
if ssl
-
ssl.close
-
else
-
sock.close
-
end
-
raise ex
-
end
-
end
-
-
# See IO#close for details.
-
1
def close
-
@svr.close
-
end
-
end
-
end
-
end
-
# frozen_string_literal: false
-
#--
-
# = Ruby-space definitions that completes C-space funcs for X509 and subclasses
-
#
-
# = Info
-
# 'OpenSSL for Ruby 2' project
-
# Copyright (C) 2002 Michal Rokos <m.rokos@sh.cvut.cz>
-
# All rights reserved.
-
#
-
# = Licence
-
# This program is licensed under the same licence as Ruby.
-
# (See the file 'LICENCE'.)
-
#++
-
-
1
module OpenSSL
-
1
module X509
-
1
class ExtensionFactory
-
1
def create_extension(*arg)
-
if arg.size > 1
-
create_ext(*arg)
-
else
-
send("create_ext_from_"+arg[0].class.name.downcase, arg[0])
-
end
-
end
-
-
1
def create_ext_from_array(ary)
-
raise ExtensionError, "unexpected array form" if ary.size > 3
-
create_ext(ary[0], ary[1], ary[2])
-
end
-
-
1
def create_ext_from_string(str) # "oid = critical, value"
-
oid, value = str.split(/=/, 2)
-
oid.strip!
-
value.strip!
-
create_ext(oid, value)
-
end
-
-
1
def create_ext_from_hash(hash)
-
create_ext(hash["oid"], hash["value"], hash["critical"])
-
end
-
end
-
-
1
class Extension
-
1
def ==(other)
-
return false unless Extension === other
-
to_der == other.to_der
-
end
-
-
1
def to_s # "oid = critical, value"
-
str = self.oid
-
str << " = "
-
str << "critical, " if self.critical?
-
str << self.value.gsub(/\n/, ", ")
-
end
-
-
1
def to_h # {"oid"=>sn|ln, "value"=>value, "critical"=>true|false}
-
{"oid"=>self.oid,"value"=>self.value,"critical"=>self.critical?}
-
end
-
-
1
def to_a
-
[ self.oid, self.value, self.critical? ]
-
end
-
end
-
-
1
class Name
-
1
module RFC2253DN
-
1
Special = ',=+<>#;'
-
1
HexChar = /[0-9a-fA-F]/
-
1
HexPair = /#{HexChar}#{HexChar}/
-
1
HexString = /#{HexPair}+/
-
1
Pair = /\\(?:[#{Special}]|\\|"|#{HexPair})/
-
1
StringChar = /[^\\"#{Special}]/
-
1
QuoteChar = /[^\\"]/
-
1
AttributeType = /[a-zA-Z][0-9a-zA-Z]*|[0-9]+(?:\.[0-9]+)*/
-
1
AttributeValue = /
-
(?!["#])((?:#{StringChar}|#{Pair})*)|
-
\#(#{HexString})|
-
"((?:#{QuoteChar}|#{Pair})*)"
-
/x
-
1
TypeAndValue = /\A(#{AttributeType})=#{AttributeValue}/
-
-
1
module_function
-
-
1
def expand_pair(str)
-
return nil unless str
-
return str.gsub(Pair){
-
pair = $&
-
case pair.size
-
when 2 then pair[1,1]
-
when 3 then Integer("0x#{pair[1,2]}").chr
-
else raise OpenSSL::X509::NameError, "invalid pair: #{str}"
-
end
-
}
-
end
-
-
1
def expand_hexstring(str)
-
return nil unless str
-
der = str.gsub(HexPair){$&.to_i(16).chr }
-
a1 = OpenSSL::ASN1.decode(der)
-
return a1.value, a1.tag
-
end
-
-
1
def expand_value(str1, str2, str3)
-
value = expand_pair(str1)
-
value, tag = expand_hexstring(str2) unless value
-
value = expand_pair(str3) unless value
-
return value, tag
-
end
-
-
1
def scan(dn)
-
str = dn
-
ary = []
-
while true
-
if md = TypeAndValue.match(str)
-
remain = md.post_match
-
type = md[1]
-
value, tag = expand_value(md[2], md[3], md[4]) rescue nil
-
if value
-
type_and_value = [type, value]
-
type_and_value.push(tag) if tag
-
ary.unshift(type_and_value)
-
if remain.length > 2 && remain[0] == ?,
-
str = remain[1..-1]
-
next
-
elsif remain.length > 2 && remain[0] == ?+
-
raise OpenSSL::X509::NameError,
-
"multi-valued RDN is not supported: #{dn}"
-
elsif remain.empty?
-
break
-
end
-
end
-
end
-
msg_dn = dn[0, dn.length - str.length] + " =>" + str
-
raise OpenSSL::X509::NameError, "malformed RDN: #{msg_dn}"
-
end
-
return ary
-
end
-
end
-
-
1
class << self
-
1
def parse_rfc2253(str, template=OBJECT_TYPE_TEMPLATE)
-
ary = OpenSSL::X509::Name::RFC2253DN.scan(str)
-
self.new(ary, template)
-
end
-
-
1
def parse_openssl(str, template=OBJECT_TYPE_TEMPLATE)
-
if str.start_with?("/")
-
# /A=B/C=D format
-
ary = str[1..-1].split("/").map { |i| i.split("=", 2) }
-
else
-
# Comma-separated
-
ary = str.split(",").map { |i| i.strip.split("=", 2) }
-
end
-
self.new(ary, template)
-
end
-
-
1
alias parse parse_openssl
-
end
-
-
1
def pretty_print(q)
-
q.object_group(self) {
-
q.text ' '
-
q.text to_s(OpenSSL::X509::Name::RFC2253)
-
}
-
end
-
end
-
-
1
class Attribute
-
1
def ==(other)
-
return false unless Attribute === other
-
to_der == other.to_der
-
end
-
end
-
-
1
class StoreContext
-
1
def cleanup
-
warn "(#{caller.first}) OpenSSL::X509::StoreContext#cleanup is deprecated with no replacement" if $VERBOSE
-
end
-
end
-
-
1
class Certificate
-
1
def pretty_print(q)
-
q.object_group(self) {
-
q.breakable
-
q.text 'subject='; q.pp self.subject; q.text ','; q.breakable
-
q.text 'issuer='; q.pp self.issuer; q.text ','; q.breakable
-
q.text 'serial='; q.pp self.serial; q.text ','; q.breakable
-
q.text 'not_before='; q.pp self.not_before; q.text ','; q.breakable
-
q.text 'not_after='; q.pp self.not_after
-
}
-
end
-
end
-
-
1
class CRL
-
1
def ==(other)
-
return false unless CRL === other
-
to_der == other.to_der
-
end
-
end
-
-
1
class Revoked
-
1
def ==(other)
-
return false unless Revoked === other
-
to_der == other.to_der
-
end
-
end
-
-
1
class Request
-
1
def ==(other)
-
return false unless Request === other
-
to_der == other.to_der
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
#
-
# optparse.rb - command-line option analysis with the OptionParser class.
-
#
-
# Author:: Nobu Nakada
-
# Documentation:: Nobu Nakada and Gavin Sinclair.
-
#
-
# See OptionParser for documentation.
-
#
-
-
-
#--
-
# == Developer Documentation (not for RDoc output)
-
#
-
# === Class tree
-
#
-
# - OptionParser:: front end
-
# - OptionParser::Switch:: each switches
-
# - OptionParser::List:: options list
-
# - OptionParser::ParseError:: errors on parsing
-
# - OptionParser::AmbiguousOption
-
# - OptionParser::NeedlessArgument
-
# - OptionParser::MissingArgument
-
# - OptionParser::InvalidOption
-
# - OptionParser::InvalidArgument
-
# - OptionParser::AmbiguousArgument
-
#
-
# === Object relationship diagram
-
#
-
# +--------------+
-
# | OptionParser |<>-----+
-
# +--------------+ | +--------+
-
# | ,-| Switch |
-
# on_head -------->+---------------+ / +--------+
-
# accept/reject -->| List |<|>-
-
# | |<|>- +----------+
-
# on ------------->+---------------+ `-| argument |
-
# : : | class |
-
# +---------------+ |==========|
-
# on_tail -------->| | |pattern |
-
# +---------------+ |----------|
-
# OptionParser.accept ->| DefaultList | |converter |
-
# reject |(shared between| +----------+
-
# | all instances)|
-
# +---------------+
-
#
-
#++
-
#
-
# == OptionParser
-
#
-
# === Introduction
-
#
-
# OptionParser is a class for command-line option analysis. It is much more
-
# advanced, yet also easier to use, than GetoptLong, and is a more Ruby-oriented
-
# solution.
-
#
-
# === Features
-
#
-
# 1. The argument specification and the code to handle it are written in the
-
# same place.
-
# 2. It can output an option summary; you don't need to maintain this string
-
# separately.
-
# 3. Optional and mandatory arguments are specified very gracefully.
-
# 4. Arguments can be automatically converted to a specified class.
-
# 5. Arguments can be restricted to a certain set.
-
#
-
# All of these features are demonstrated in the examples below. See
-
# #make_switch for full documentation.
-
#
-
# === Minimal example
-
#
-
# require 'optparse'
-
#
-
# options = {}
-
# OptionParser.new do |opts|
-
# opts.banner = "Usage: example.rb [options]"
-
#
-
# opts.on("-v", "--[no-]verbose", "Run verbosely") do |v|
-
# options[:verbose] = v
-
# end
-
# end.parse!
-
#
-
# p options
-
# p ARGV
-
#
-
# === Generating Help
-
#
-
# OptionParser can be used to automatically generate help for the commands you
-
# write:
-
#
-
# require 'optparse'
-
#
-
# Options = Struct.new(:name)
-
#
-
# class Parser
-
# def self.parse(options)
-
# args = Options.new("world")
-
#
-
# opt_parser = OptionParser.new do |opts|
-
# opts.banner = "Usage: example.rb [options]"
-
#
-
# opts.on("-nNAME", "--name=NAME", "Name to say hello to") do |n|
-
# args.name = n
-
# end
-
#
-
# opts.on("-h", "--help", "Prints this help") do
-
# puts opts
-
# exit
-
# end
-
# end
-
#
-
# opt_parser.parse!(options)
-
# return args
-
# end
-
# end
-
# options = Parser.parse %w[--help]
-
#
-
# #=>
-
# # Usage: example.rb [options]
-
# # -n, --name=NAME Name to say hello to
-
# # -h, --help Prints this help
-
#
-
# === Required Arguments
-
#
-
# For options that require an argument, option specification strings may include an
-
# option name in all caps. If an option is used without the required argument,
-
# an exception will be raised.
-
#
-
# require 'optparse'
-
#
-
# options = {}
-
# OptionParser.new do |parser|
-
# parser.on("-r", "--require LIBRARY",
-
# "Require the LIBRARY before executing your script") do |lib|
-
# puts "You required #{lib}!"
-
# end
-
# end.parse!
-
#
-
# Used:
-
#
-
# $ ruby optparse-test.rb -r
-
# optparse-test.rb:9:in `<main>': missing argument: -r (OptionParser::MissingArgument)
-
# $ ruby optparse-test.rb -r my-library
-
# You required my-library!
-
#
-
# === Type Coercion
-
#
-
# OptionParser supports the ability to coerce command line arguments
-
# into objects for us.
-
#
-
# OptionParser comes with a few ready-to-use kinds of type
-
# coercion. They are:
-
#
-
# - Date -- Anything accepted by +Date.parse+
-
# - DateTime -- Anything accepted by +DateTime.parse+
-
# - Time -- Anything accepted by +Time.httpdate+ or +Time.parse+
-
# - URI -- Anything accepted by +URI.parse+
-
# - Shellwords -- Anything accepted by +Shellwords.shellwords+
-
# - String -- Any non-empty string
-
# - Integer -- Any integer. Will convert octal. (e.g. 124, -3, 040)
-
# - Float -- Any float. (e.g. 10, 3.14, -100E+13)
-
# - Numeric -- Any integer, float, or rational (1, 3.4, 1/3)
-
# - DecimalInteger -- Like +Integer+, but no octal format.
-
# - OctalInteger -- Like +Integer+, but no decimal format.
-
# - DecimalNumeric -- Decimal integer or float.
-
# - TrueClass -- Accepts '+, yes, true, -, no, false' and
-
# defaults as +true+
-
# - FalseClass -- Same as +TrueClass+, but defaults to +false+
-
# - Array -- Strings separated by ',' (e.g. 1,2,3)
-
# - Regexp -- Regular expressions. Also includes options.
-
#
-
# We can also add our own coercions, which we will cover below.
-
#
-
# ==== Using Built-in Conversions
-
#
-
# As an example, the built-in +Time+ conversion is used. The other built-in
-
# conversions behave in the same way.
-
# OptionParser will attempt to parse the argument
-
# as a +Time+. If it succeeds, that time will be passed to the
-
# handler block. Otherwise, an exception will be raised.
-
#
-
# require 'optparse'
-
# require 'optparse/time'
-
# OptionParser.new do |parser|
-
# parser.on("-t", "--time [TIME]", Time, "Begin execution at given time") do |time|
-
# p time
-
# end
-
# end.parse!
-
#
-
# Used:
-
#
-
# $ ruby optparse-test.rb -t nonsense
-
# ... invalid argument: -t nonsense (OptionParser::InvalidArgument)
-
# $ ruby optparse-test.rb -t 10-11-12
-
# 2010-11-12 00:00:00 -0500
-
# $ ruby optparse-test.rb -t 9:30
-
# 2014-08-13 09:30:00 -0400
-
#
-
# ==== Creating Custom Conversions
-
#
-
# The +accept+ method on OptionParser may be used to create converters.
-
# It specifies which conversion block to call whenever a class is specified.
-
# The example below uses it to fetch a +User+ object before the +on+ handler receives it.
-
#
-
# require 'optparse'
-
#
-
# User = Struct.new(:id, :name)
-
#
-
# def find_user id
-
# not_found = ->{ raise "No User Found for id #{id}" }
-
# [ User.new(1, "Sam"),
-
# User.new(2, "Gandalf") ].find(not_found) do |u|
-
# u.id == id
-
# end
-
# end
-
#
-
# op = OptionParser.new
-
# op.accept(User) do |user_id|
-
# find_user user_id.to_i
-
# end
-
#
-
# op.on("--user ID", User) do |user|
-
# puts user
-
# end
-
#
-
# op.parse!
-
#
-
# Used:
-
#
-
# $ ruby optparse-test.rb --user 1
-
# #<struct User id=1, name="Sam">
-
# $ ruby optparse-test.rb --user 2
-
# #<struct User id=2, name="Gandalf">
-
# $ ruby optparse-test.rb --user 3
-
# optparse-test.rb:15:in `block in find_user': No User Found for id 3 (RuntimeError)
-
#
-
# === Store options to a Hash
-
#
-
# The +into+ option of +order+, +parse+ and so on methods stores command line options into a Hash.
-
#
-
# require 'optparse'
-
#
-
# params = {}
-
# OptionParser.new do |opts|
-
# opts.on('-a')
-
# opts.on('-b NUM', Integer)
-
# opts.on('-v', '--verbose')
-
# end.parse!(into: params)
-
#
-
# p params
-
#
-
# Used:
-
#
-
# $ ruby optparse-test.rb -a
-
# {:a=>true}
-
# $ ruby optparse-test.rb -a -v
-
# {:a=>true, :verbose=>true}
-
# $ ruby optparse-test.rb -a -b 100
-
# {:a=>true, :b=>100}
-
#
-
# === Complete example
-
#
-
# The following example is a complete Ruby program. You can run it and see the
-
# effect of specifying various options. This is probably the best way to learn
-
# the features of +optparse+.
-
#
-
# require 'optparse'
-
# require 'optparse/time'
-
# require 'ostruct'
-
# require 'pp'
-
#
-
# class OptparseExample
-
# Version = '1.0.0'
-
#
-
# CODES = %w[iso-2022-jp shift_jis euc-jp utf8 binary]
-
# CODE_ALIASES = { "jis" => "iso-2022-jp", "sjis" => "shift_jis" }
-
#
-
# class ScriptOptions
-
# attr_accessor :library, :inplace, :encoding, :transfer_type,
-
# :verbose, :extension, :delay, :time, :record_separator,
-
# :list
-
#
-
# def initialize
-
# self.library = []
-
# self.inplace = false
-
# self.encoding = "utf8"
-
# self.transfer_type = :auto
-
# self.verbose = false
-
# end
-
#
-
# def define_options(parser)
-
# parser.banner = "Usage: example.rb [options]"
-
# parser.separator ""
-
# parser.separator "Specific options:"
-
#
-
# # add additional options
-
# perform_inplace_option(parser)
-
# delay_execution_option(parser)
-
# execute_at_time_option(parser)
-
# specify_record_separator_option(parser)
-
# list_example_option(parser)
-
# specify_encoding_option(parser)
-
# optional_option_argument_with_keyword_completion_option(parser)
-
# boolean_verbose_option(parser)
-
#
-
# parser.separator ""
-
# parser.separator "Common options:"
-
# # No argument, shows at tail. This will print an options summary.
-
# # Try it and see!
-
# parser.on_tail("-h", "--help", "Show this message") do
-
# puts parser
-
# exit
-
# end
-
# # Another typical switch to print the version.
-
# parser.on_tail("--version", "Show version") do
-
# puts Version
-
# exit
-
# end
-
# end
-
#
-
# def perform_inplace_option(parser)
-
# # Specifies an optional option argument
-
# parser.on("-i", "--inplace [EXTENSION]",
-
# "Edit ARGV files in place",
-
# "(make backup if EXTENSION supplied)") do |ext|
-
# self.inplace = true
-
# self.extension = ext || ''
-
# self.extension.sub!(/\A\.?(?=.)/, ".") # Ensure extension begins with dot.
-
# end
-
# end
-
#
-
# def delay_execution_option(parser)
-
# # Cast 'delay' argument to a Float.
-
# parser.on("--delay N", Float, "Delay N seconds before executing") do |n|
-
# self.delay = n
-
# end
-
# end
-
#
-
# def execute_at_time_option(parser)
-
# # Cast 'time' argument to a Time object.
-
# parser.on("-t", "--time [TIME]", Time, "Begin execution at given time") do |time|
-
# self.time = time
-
# end
-
# end
-
#
-
# def specify_record_separator_option(parser)
-
# # Cast to octal integer.
-
# parser.on("-F", "--irs [OCTAL]", OptionParser::OctalInteger,
-
# "Specify record separator (default \\0)") do |rs|
-
# self.record_separator = rs
-
# end
-
# end
-
#
-
# def list_example_option(parser)
-
# # List of arguments.
-
# parser.on("--list x,y,z", Array, "Example 'list' of arguments") do |list|
-
# self.list = list
-
# end
-
# end
-
#
-
# def specify_encoding_option(parser)
-
# # Keyword completion. We are specifying a specific set of arguments (CODES
-
# # and CODE_ALIASES - notice the latter is a Hash), and the user may provide
-
# # the shortest unambiguous text.
-
# code_list = (CODE_ALIASES.keys + CODES).join(', ')
-
# parser.on("--code CODE", CODES, CODE_ALIASES, "Select encoding",
-
# "(#{code_list})") do |encoding|
-
# self.encoding = encoding
-
# end
-
# end
-
#
-
# def optional_option_argument_with_keyword_completion_option(parser)
-
# # Optional '--type' option argument with keyword completion.
-
# parser.on("--type [TYPE]", [:text, :binary, :auto],
-
# "Select transfer type (text, binary, auto)") do |t|
-
# self.transfer_type = t
-
# end
-
# end
-
#
-
# def boolean_verbose_option(parser)
-
# # Boolean switch.
-
# parser.on("-v", "--[no-]verbose", "Run verbosely") do |v|
-
# self.verbose = v
-
# end
-
# end
-
# end
-
#
-
# #
-
# # Return a structure describing the options.
-
# #
-
# def parse(args)
-
# # The options specified on the command line will be collected in
-
# # *options*.
-
#
-
# @options = ScriptOptions.new
-
# @args = OptionParser.new do |parser|
-
# @options.define_options(parser)
-
# parser.parse!(args)
-
# end
-
# @options
-
# end
-
#
-
# attr_reader :parser, :options
-
# end # class OptparseExample
-
#
-
# example = OptparseExample.new
-
# options = example.parse(ARGV)
-
# pp options # example.options
-
# pp ARGV
-
#
-
# === Shell Completion
-
#
-
# For modern shells (e.g. bash, zsh, etc.), you can use shell
-
# completion for command line options.
-
#
-
# === Further documentation
-
#
-
# The above examples should be enough to learn how to use this class. If you
-
# have any questions, file a ticket at http://bugs.ruby-lang.org.
-
#
-
1
class OptionParser
-
# :stopdoc:
-
1
NoArgument = [NO_ARGUMENT = :NONE, nil].freeze
-
1
RequiredArgument = [REQUIRED_ARGUMENT = :REQUIRED, true].freeze
-
1
OptionalArgument = [OPTIONAL_ARGUMENT = :OPTIONAL, false].freeze
-
# :startdoc:
-
-
#
-
# Keyword completion module. This allows partial arguments to be specified
-
# and resolved against a list of acceptable values.
-
#
-
1
module Completion
-
1
def self.regexp(key, icase)
-
Regexp.new('\A' + Regexp.quote(key).gsub(/\w+\b/, '\&\w*'), icase)
-
end
-
-
1
def self.candidate(key, icase = false, pat = nil, &block)
-
pat ||= Completion.regexp(key, icase)
-
candidates = []
-
block.call do |k, *v|
-
(if Regexp === k
-
kn = ""
-
k === key
-
else
-
kn = defined?(k.id2name) ? k.id2name : k
-
pat === kn
-
end) or next
-
v << k if v.empty?
-
candidates << [k, v, kn]
-
end
-
candidates
-
end
-
-
1
def candidate(key, icase = false, pat = nil)
-
Completion.candidate(key, icase, pat, &method(:each))
-
end
-
-
1
public
-
1
def complete(key, icase = false, pat = nil)
-
candidates = candidate(key, icase, pat, &method(:each)).sort_by {|k, v, kn| kn.size}
-
if candidates.size == 1
-
canon, sw, * = candidates[0]
-
elsif candidates.size > 1
-
canon, sw, cn = candidates.shift
-
candidates.each do |k, v, kn|
-
next if sw == v
-
if String === cn and String === kn
-
if cn.rindex(kn, 0)
-
canon, sw, cn = k, v, kn
-
next
-
elsif kn.rindex(cn, 0)
-
next
-
end
-
end
-
throw :ambiguous, key
-
end
-
end
-
if canon
-
block_given? or return key, *sw
-
yield(key, *sw)
-
end
-
end
-
-
1
def convert(opt = nil, val = nil, *)
-
val
-
end
-
end
-
-
-
#
-
# Map from option/keyword string to object with completion.
-
#
-
1
class OptionMap < Hash
-
1
include Completion
-
end
-
-
-
#
-
# Individual switch class. Not important to the user.
-
#
-
# Defined within Switch are several Switch-derived classes: NoArgument,
-
# RequiredArgument, etc.
-
#
-
1
class Switch
-
1
attr_reader :pattern, :conv, :short, :long, :arg, :desc, :block
-
-
#
-
# Guesses argument style from +arg+. Returns corresponding
-
# OptionParser::Switch class (OptionalArgument, etc.).
-
#
-
1
def self.guess(arg)
-
3
case arg
-
when ""
-
t = self
-
when /\A=?\[/
-
t = Switch::OptionalArgument
-
when /\A\s+\[/
-
t = Switch::PlacedArgument
-
else
-
3
t = Switch::RequiredArgument
-
end
-
3
self >= t or incompatible_argument_styles(arg, t)
-
3
t
-
end
-
-
1
def self.incompatible_argument_styles(arg, t)
-
raise(ArgumentError, "#{arg}: incompatible argument styles\n #{self}, #{t}",
-
ParseError.filter_backtrace(caller(2)))
-
end
-
-
1
def self.pattern
-
NilClass
-
end
-
-
1
def initialize(pattern = nil, conv = nil,
-
short = nil, long = nil, arg = nil,
-
6
desc = ([] if short or long), block = nil, &_block)
-
13
raise if Array === pattern
-
13
block ||= _block
-
@pattern, @conv, @short, @long, @arg, @desc, @block =
-
13
pattern, conv, short, long, arg, desc, block
-
end
-
-
#
-
# Parses +arg+ and returns rest of +arg+ and matched portion to the
-
# argument pattern. Yields when the pattern doesn't match substring.
-
#
-
1
def parse_arg(arg)
-
pattern or return nil, [arg]
-
unless m = pattern.match(arg)
-
yield(InvalidArgument, arg)
-
return arg, []
-
end
-
if String === m
-
m = [s = m]
-
else
-
m = m.to_a
-
s = m[0]
-
return nil, m unless String === s
-
end
-
raise InvalidArgument, arg unless arg.rindex(s, 0)
-
return nil, m if s.length == arg.length
-
yield(InvalidArgument, arg) # didn't match whole arg
-
return arg[s.length..-1], m
-
end
-
1
private :parse_arg
-
-
#
-
# Parses argument, converts and returns +arg+, +block+ and result of
-
# conversion. Yields at semi-error condition instead of raising an
-
# exception.
-
#
-
1
def conv_arg(arg, val = [])
-
if conv
-
val = conv.call(*val)
-
else
-
val = proc {|v| v}.call(*val)
-
end
-
return arg, block, val
-
end
-
1
private :conv_arg
-
-
#
-
# Produces the summary text. Each line of the summary is yielded to the
-
# block (without newline).
-
#
-
# +sdone+:: Already summarized short style options keyed hash.
-
# +ldone+:: Already summarized long style options keyed hash.
-
# +width+:: Width of left side (option part). In other words, the right
-
# side (description part) starts after +width+ columns.
-
# +max+:: Maximum width of left side -> the options are filled within
-
# +max+ columns.
-
# +indent+:: Prefix string indents all summarized lines.
-
#
-
1
def summarize(sdone = {}, ldone = {}, width = 1, max = width - 1, indent = "")
-
sopts, lopts = [], [], nil
-
@short.each {|s| sdone.fetch(s) {sopts << s}; sdone[s] = true} if @short
-
@long.each {|s| ldone.fetch(s) {lopts << s}; ldone[s] = true} if @long
-
return if sopts.empty? and lopts.empty? # completely hidden
-
-
left = [sopts.join(', ')]
-
right = desc.dup
-
-
while s = lopts.shift
-
l = left[-1].length + s.length
-
l += arg.length if left.size == 1 && arg
-
l < max or sopts.empty? or left << +''
-
left[-1] << (left[-1].empty? ? ' ' * 4 : ', ') << s
-
end
-
-
if arg
-
left[0] << (left[1] ? arg.sub(/\A(\[?)=/, '\1') + ',' : arg)
-
end
-
mlen = left.collect {|ss| ss.length}.max.to_i
-
while mlen > width and l = left.shift
-
mlen = left.collect {|ss| ss.length}.max.to_i if l.length == mlen
-
if l.length < width and (r = right[0]) and !r.empty?
-
l = l.to_s.ljust(width) + ' ' + r
-
right.shift
-
end
-
yield(indent + l)
-
end
-
-
while begin l = left.shift; r = right.shift; l or r end
-
l = l.to_s.ljust(width) + ' ' + r if r and !r.empty?
-
yield(indent + l)
-
end
-
-
self
-
end
-
-
1
def add_banner(to) # :nodoc:
-
unless @short or @long
-
s = desc.join
-
to << " [" + s + "]..." unless s.empty?
-
end
-
to
-
end
-
-
1
def match_nonswitch?(str) # :nodoc:
-
@pattern =~ str unless @short or @long
-
end
-
-
#
-
# Main name of the switch.
-
#
-
1
def switch_name
-
(long.first || short.first).sub(/\A-+(?:\[no-\])?/, '')
-
end
-
-
1
def compsys(sdone, ldone) # :nodoc:
-
sopts, lopts = [], []
-
@short.each {|s| sdone.fetch(s) {sopts << s}; sdone[s] = true} if @short
-
@long.each {|s| ldone.fetch(s) {lopts << s}; ldone[s] = true} if @long
-
return if sopts.empty? and lopts.empty? # completely hidden
-
-
(sopts+lopts).each do |opt|
-
# "(-x -c -r)-l[left justify]"
-
if /^--\[no-\](.+)$/ =~ opt
-
o = $1
-
yield("--#{o}", desc.join(""))
-
yield("--no-#{o}", desc.join(""))
-
else
-
yield("#{opt}", desc.join(""))
-
end
-
end
-
end
-
-
#
-
# Switch that takes no arguments.
-
#
-
1
class NoArgument < self
-
-
#
-
# Raises an exception if any arguments given.
-
#
-
1
def parse(arg, argv)
-
yield(NeedlessArgument, arg) if arg
-
conv_arg(arg)
-
end
-
-
1
def self.incompatible_argument_styles(*)
-
end
-
-
1
def self.pattern
-
3
Object
-
end
-
end
-
-
#
-
# Switch that takes an argument.
-
#
-
1
class RequiredArgument < self
-
-
#
-
# Raises an exception if argument is not present.
-
#
-
1
def parse(arg, argv)
-
unless arg
-
raise MissingArgument if argv.empty?
-
arg = argv.shift
-
end
-
conv_arg(*parse_arg(arg, &method(:raise)))
-
end
-
end
-
-
#
-
# Switch that can omit argument.
-
#
-
1
class OptionalArgument < self
-
-
#
-
# Parses argument if given, or uses default value.
-
#
-
1
def parse(arg, argv, &error)
-
if arg
-
conv_arg(*parse_arg(arg, &error))
-
else
-
conv_arg(arg)
-
end
-
end
-
end
-
-
#
-
# Switch that takes an argument, which does not begin with '-'.
-
#
-
1
class PlacedArgument < self
-
-
#
-
# Returns nil if argument is not present or begins with '-'.
-
#
-
1
def parse(arg, argv, &error)
-
if !(val = arg) and (argv.empty? or /\A-/ =~ (val = argv[0]))
-
return nil, block, nil
-
end
-
opt = (val = parse_arg(val, &error))[1]
-
val = conv_arg(*val)
-
if opt and !arg
-
argv.shift
-
else
-
val[0] = nil
-
end
-
val
-
end
-
end
-
end
-
-
#
-
# Simple option list providing mapping from short and/or long option
-
# string to OptionParser::Switch and mapping from acceptable argument to
-
# matching pattern and converter pair. Also provides summary feature.
-
#
-
1
class List
-
# Map from acceptable argument types to pattern and converter pairs.
-
1
attr_reader :atype
-
-
# Map from short style option switches to actual switch objects.
-
1
attr_reader :short
-
-
# Map from long style option switches to actual switch objects.
-
1
attr_reader :long
-
-
# List of all switches and summary string.
-
1
attr_reader :list
-
-
#
-
# Just initializes all instance variables.
-
#
-
1
def initialize
-
3
@atype = {}
-
3
@short = OptionMap.new
-
3
@long = OptionMap.new
-
3
@list = []
-
end
-
-
#
-
# See OptionParser.accept.
-
#
-
1
def accept(t, pat = /.*/m, &block)
-
13
if pat
-
13
pat.respond_to?(:match) or
-
raise TypeError, "has no `match'", ParseError.filter_backtrace(caller(2))
-
else
-
pat = t if t.respond_to?(:match)
-
end
-
13
unless block
-
block = pat.method(:convert).to_proc if pat.respond_to?(:convert)
-
end
-
13
@atype[t] = [pat, block]
-
end
-
-
#
-
# See OptionParser.reject.
-
#
-
1
def reject(t)
-
@atype.delete(t)
-
end
-
-
#
-
# Adds +sw+ according to +sopts+, +lopts+ and +nlopts+.
-
#
-
# +sw+:: OptionParser::Switch instance to be added.
-
# +sopts+:: Short style option list.
-
# +lopts+:: Long style option list.
-
# +nlopts+:: Negated long style options list.
-
#
-
1
def update(sw, sopts, lopts, nsw = nil, nlopts = nil)
-
15
sopts.each {|o| @short[o] = sw} if sopts
-
16
lopts.each {|o| @long[o] = sw} if lopts
-
9
nlopts.each {|o| @long[o] = nsw} if nsw and nlopts
-
9
used = @short.invert.update(@long.invert)
-
45
@list.delete_if {|o| Switch === o and !used[o]}
-
end
-
1
private :update
-
-
#
-
# Inserts +switch+ at the head of the list, and associates short, long
-
# and negated long options. Arguments are:
-
#
-
# +switch+:: OptionParser::Switch instance to be inserted.
-
# +short_opts+:: List of short style options.
-
# +long_opts+:: List of long style options.
-
# +nolong_opts+:: List of long style options with "no-" prefix.
-
#
-
# prepend(switch, short_opts, long_opts, nolong_opts)
-
#
-
1
def prepend(*args)
-
update(*args)
-
@list.unshift(args[0])
-
end
-
-
#
-
# Appends +switch+ at the tail of the list, and associates short, long
-
# and negated long options. Arguments are:
-
#
-
# +switch+:: OptionParser::Switch instance to be inserted.
-
# +short_opts+:: List of short style options.
-
# +long_opts+:: List of long style options.
-
# +nolong_opts+:: List of long style options with "no-" prefix.
-
#
-
# append(switch, short_opts, long_opts, nolong_opts)
-
#
-
1
def append(*args)
-
9
update(*args)
-
9
@list.push(args[0])
-
end
-
-
#
-
# Searches +key+ in +id+ list. The result is returned or yielded if a
-
# block is given. If it isn't found, nil is returned.
-
#
-
1
def search(id, key)
-
90
if list = __send__(id)
-
171
val = list.fetch(key) {return nil}
-
9
block_given? ? yield(val) : val
-
end
-
end
-
-
#
-
# Searches list +id+ for +opt+ and the optional patterns for completion
-
# +pat+. If +icase+ is true, the search is case insensitive. The result
-
# is returned or yielded if a block is given. If it isn't found, nil is
-
# returned.
-
#
-
1
def complete(id, opt, icase = false, *pat, &block)
-
__send__(id).complete(opt, icase, *pat, &block)
-
end
-
-
1
def get_candidates(id)
-
yield __send__(id).keys
-
end
-
-
#
-
# Iterates over each option, passing the option to the +block+.
-
#
-
1
def each_option(&block)
-
list.each(&block)
-
end
-
-
#
-
# Creates the summary table, passing each line to the +block+ (without
-
# newline). The arguments +args+ are passed along to the summarize
-
# method which is called on every option.
-
#
-
1
def summarize(*args, &block)
-
sum = []
-
list.reverse_each do |opt|
-
if opt.respond_to?(:summarize) # perhaps OptionParser::Switch
-
s = []
-
opt.summarize(*args) {|l| s << l}
-
sum.concat(s.reverse)
-
elsif !opt or opt.empty?
-
sum << ""
-
elsif opt.respond_to?(:each_line)
-
sum.concat([*opt.each_line].reverse)
-
else
-
sum.concat([*opt.each].reverse)
-
end
-
end
-
sum.reverse_each(&block)
-
end
-
-
1
def add_banner(to) # :nodoc:
-
list.each do |opt|
-
if opt.respond_to?(:add_banner)
-
opt.add_banner(to)
-
end
-
end
-
to
-
end
-
-
1
def compsys(*args, &block) # :nodoc:
-
list.each do |opt|
-
if opt.respond_to?(:compsys)
-
opt.compsys(*args, &block)
-
end
-
end
-
end
-
end
-
-
#
-
# Hash with completion search feature. See OptionParser::Completion.
-
#
-
1
class CompletingHash < Hash
-
1
include Completion
-
-
#
-
# Completion for hash key.
-
#
-
1
def match(key)
-
*values = fetch(key) {
-
raise AmbiguousArgument, catch(:ambiguous) {return complete(key)}
-
}
-
return key, *values
-
end
-
end
-
-
# :stopdoc:
-
-
#
-
# Enumeration of acceptable argument styles. Possible values are:
-
#
-
# NO_ARGUMENT:: The switch takes no arguments. (:NONE)
-
# REQUIRED_ARGUMENT:: The switch requires an argument. (:REQUIRED)
-
# OPTIONAL_ARGUMENT:: The switch requires an optional argument. (:OPTIONAL)
-
#
-
# Use like --switch=argument (long style) or -Xargument (short style). For
-
# short style, only portion matched to argument pattern is treated as
-
# argument.
-
#
-
1
ArgumentStyle = {}
-
3
NoArgument.each {|el| ArgumentStyle[el] = Switch::NoArgument}
-
3
RequiredArgument.each {|el| ArgumentStyle[el] = Switch::RequiredArgument}
-
3
OptionalArgument.each {|el| ArgumentStyle[el] = Switch::OptionalArgument}
-
1
ArgumentStyle.freeze
-
-
#
-
# Switches common used such as '--', and also provides default
-
# argument classes
-
#
-
1
DefaultList = List.new
-
1
DefaultList.short['-'] = Switch::NoArgument.new {}
-
1
DefaultList.long[''] = Switch::NoArgument.new {throw :terminate}
-
-
-
1
COMPSYS_HEADER = <<'XXX' # :nodoc:
-
-
typeset -A opt_args
-
local context state line
-
-
_arguments -s -S \
-
XXX
-
-
1
def compsys(to, name = File.basename($0)) # :nodoc:
-
to << "#compdef #{name}\n"
-
to << COMPSYS_HEADER
-
visit(:compsys, {}, {}) {|o, d|
-
to << %Q[ "#{o}[#{d.gsub(/[\"\[\]]/, '\\\\\&')}]" \\\n]
-
}
-
to << " '*:file:_files' && return 0\n"
-
end
-
-
#
-
# Default options for ARGV, which never appear in option summary.
-
#
-
1
Officious = {}
-
-
#
-
# --help
-
# Shows option summary.
-
#
-
1
Officious['help'] = proc do |parser|
-
1
Switch::NoArgument.new do |arg|
-
puts parser.help
-
exit
-
end
-
end
-
-
#
-
# --*-completion-bash=WORD
-
# Shows candidates for command line completion.
-
#
-
1
Officious['*-completion-bash'] = proc do |parser|
-
1
Switch::RequiredArgument.new do |arg|
-
puts parser.candidate(arg)
-
exit
-
end
-
end
-
-
#
-
# --*-completion-zsh[=NAME:FILE]
-
# Creates zsh completion file.
-
#
-
1
Officious['*-completion-zsh'] = proc do |parser|
-
1
Switch::OptionalArgument.new do |arg|
-
parser.compsys(STDOUT, arg)
-
exit
-
end
-
end
-
-
#
-
# --version
-
# Shows version string if Version is defined.
-
#
-
1
Officious['version'] = proc do |parser|
-
1
Switch::OptionalArgument.new do |pkg|
-
if pkg
-
begin
-
require 'optparse/version'
-
rescue LoadError
-
else
-
show_version(*pkg.split(/,/)) or
-
abort("#{parser.program_name}: no version found in package #{pkg}")
-
exit
-
end
-
end
-
v = parser.ver or abort("#{parser.program_name}: version unknown")
-
puts v
-
exit
-
end
-
end
-
-
# :startdoc:
-
-
#
-
# Class methods
-
#
-
-
#
-
# Initializes a new instance and evaluates the optional block in context
-
# of the instance. Arguments +args+ are passed to #new, see there for
-
# description of parameters.
-
#
-
# This method is *deprecated*, its behavior corresponds to the older #new
-
# method.
-
#
-
1
def self.with(*args, &block)
-
opts = new(*args)
-
opts.instance_eval(&block)
-
opts
-
end
-
-
#
-
# Returns an incremented value of +default+ according to +arg+.
-
#
-
1
def self.inc(arg, default = nil)
-
case arg
-
when Integer
-
arg.nonzero?
-
when nil
-
default.to_i + 1
-
end
-
end
-
1
def inc(*args)
-
self.class.inc(*args)
-
end
-
-
#
-
# Initializes the instance and yields itself if called with a block.
-
#
-
# +banner+:: Banner message.
-
# +width+:: Summary width.
-
# +indent+:: Summary indent.
-
#
-
1
def initialize(banner = nil, width = 32, indent = ' ' * 4)
-
1
@stack = [DefaultList, List.new, List.new]
-
1
@program_name = nil
-
1
@banner = banner
-
1
@summary_width = width
-
1
@summary_indent = indent
-
1
@default_argv = ARGV
-
1
add_officious
-
1
yield self if block_given?
-
end
-
-
1
def add_officious # :nodoc:
-
1
list = base()
-
1
Officious.each do |opt, block|
-
4
list.long[opt] ||= block.call(self)
-
end
-
end
-
-
#
-
# Terminates option parsing. Optional parameter +arg+ is a string pushed
-
# back to be the first non-option argument.
-
#
-
1
def terminate(arg = nil)
-
self.class.terminate(arg)
-
end
-
1
def self.terminate(arg = nil)
-
throw :terminate, arg
-
end
-
-
1
@stack = [DefaultList]
-
14
def self.top() DefaultList end
-
-
#
-
# Directs to accept specified class +t+. The argument string is passed to
-
# the block in which it should be converted to the desired class.
-
#
-
# +t+:: Argument class specifier, any object including Class.
-
# +pat+:: Pattern for argument, defaults to +t+ if it responds to match.
-
#
-
# accept(t, pat, &block)
-
#
-
1
def accept(*args, &blk) top.accept(*args, &blk) end
-
#
-
# See #accept.
-
#
-
14
def self.accept(*args, &blk) top.accept(*args, &blk) end
-
-
#
-
# Directs to reject specified class argument.
-
#
-
# +t+:: Argument class specifier, any object including Class.
-
#
-
# reject(t)
-
#
-
1
def reject(*args, &blk) top.reject(*args, &blk) end
-
#
-
# See #reject.
-
#
-
1
def self.reject(*args, &blk) top.reject(*args, &blk) end
-
-
#
-
# Instance methods
-
#
-
-
# Heading banner preceding summary.
-
1
attr_writer :banner
-
-
# Program name to be emitted in error message and default banner,
-
# defaults to $0.
-
1
attr_writer :program_name
-
-
# Width for option list portion of summary. Must be Numeric.
-
1
attr_accessor :summary_width
-
-
# Indentation for summary. Must be String (or have + String method).
-
1
attr_accessor :summary_indent
-
-
# Strings to be parsed in default.
-
1
attr_accessor :default_argv
-
-
#
-
# Heading banner preceding summary.
-
#
-
1
def banner
-
unless @banner
-
@banner = +"Usage: #{program_name} [options]"
-
visit(:add_banner, @banner)
-
end
-
@banner
-
end
-
-
#
-
# Program name to be emitted in error message and default banner, defaults
-
# to $0.
-
#
-
1
def program_name
-
@program_name || File.basename($0, '.*')
-
end
-
-
# for experimental cascading :-)
-
1
alias set_banner banner=
-
1
alias set_program_name program_name=
-
1
alias set_summary_width summary_width=
-
1
alias set_summary_indent summary_indent=
-
-
# Version
-
1
attr_writer :version
-
# Release code
-
1
attr_writer :release
-
-
#
-
# Version
-
#
-
1
def version
-
(defined?(@version) && @version) || (defined?(::Version) && ::Version)
-
end
-
-
#
-
# Release code
-
#
-
1
def release
-
(defined?(@release) && @release) || (defined?(::Release) && ::Release) || (defined?(::RELEASE) && ::RELEASE)
-
end
-
-
#
-
# Returns version string from program_name, version and release.
-
#
-
1
def ver
-
if v = version
-
str = +"#{program_name} #{[v].join('.')}"
-
str << " (#{v})" if v = release
-
str
-
end
-
end
-
-
1
def warn(mesg = $!)
-
super("#{program_name}: #{mesg}")
-
end
-
-
1
def abort(mesg = $!)
-
super("#{program_name}: #{mesg}")
-
end
-
-
#
-
# Subject of #on / #on_head, #accept / #reject
-
#
-
1
def top
-
9
@stack[-1]
-
end
-
-
#
-
# Subject of #on_tail.
-
#
-
1
def base
-
1
@stack[1]
-
end
-
-
#
-
# Pushes a new List.
-
#
-
1
def new
-
@stack.push(List.new)
-
if block_given?
-
yield self
-
else
-
self
-
end
-
end
-
-
#
-
# Removes the last List.
-
#
-
1
def remove
-
@stack.pop
-
end
-
-
#
-
# Puts option summary into +to+ and returns +to+. Yields each line if
-
# a block is given.
-
#
-
# +to+:: Output destination, which must have method <<. Defaults to [].
-
# +width+:: Width of left side, defaults to @summary_width.
-
# +max+:: Maximum length allowed for left side, defaults to +width+ - 1.
-
# +indent+:: Indentation, defaults to @summary_indent.
-
#
-
1
def summarize(to = [], width = @summary_width, max = width - 1, indent = @summary_indent, &blk)
-
nl = "\n"
-
blk ||= proc {|l| to << (l.index(nl, -1) ? l : l + nl)}
-
visit(:summarize, {}, {}, width, max, indent, &blk)
-
to
-
end
-
-
#
-
# Returns option summary string.
-
#
-
1
def help; summarize("#{banner}".sub(/\n?\z/, "\n")) end
-
1
alias to_s help
-
-
#
-
# Returns option summary list.
-
#
-
1
def to_a; summarize("#{banner}".split(/^/)) end
-
-
#
-
# Checks if an argument is given twice, in which case an ArgumentError is
-
# raised. Called from OptionParser#switch only.
-
#
-
# +obj+:: New argument.
-
# +prv+:: Previously specified argument.
-
# +msg+:: Exception message.
-
#
-
1
def notwice(obj, prv, msg)
-
5
unless !prv or prv == obj
-
raise(ArgumentError, "argument #{msg} given twice: #{obj}",
-
ParseError.filter_backtrace(caller(2)))
-
end
-
5
obj
-
end
-
1
private :notwice
-
-
1
SPLAT_PROC = proc {|*a| a.length <= 1 ? a.first : a} # :nodoc:
-
#
-
# Creates an OptionParser::Switch from the parameters. The parsed argument
-
# value is passed to the given block, where it can be processed.
-
#
-
# See at the beginning of OptionParser for some full examples.
-
#
-
# +opts+ can include the following elements:
-
#
-
# [Argument style:]
-
# One of the following:
-
# :NONE, :REQUIRED, :OPTIONAL
-
#
-
# [Argument pattern:]
-
# Acceptable option argument format, must be pre-defined with
-
# OptionParser.accept or OptionParser#accept, or Regexp. This can appear
-
# once or assigned as String if not present, otherwise causes an
-
# ArgumentError. Examples:
-
# Float, Time, Array
-
#
-
# [Possible argument values:]
-
# Hash or Array.
-
# [:text, :binary, :auto]
-
# %w[iso-2022-jp shift_jis euc-jp utf8 binary]
-
# { "jis" => "iso-2022-jp", "sjis" => "shift_jis" }
-
#
-
# [Long style switch:]
-
# Specifies a long style switch which takes a mandatory, optional or no
-
# argument. It's a string of the following form:
-
# "--switch=MANDATORY" or "--switch MANDATORY"
-
# "--switch[=OPTIONAL]"
-
# "--switch"
-
#
-
# [Short style switch:]
-
# Specifies short style switch which takes a mandatory, optional or no
-
# argument. It's a string of the following form:
-
# "-xMANDATORY"
-
# "-x[OPTIONAL]"
-
# "-x"
-
# There is also a special form which matches character range (not full
-
# set of regular expression):
-
# "-[a-z]MANDATORY"
-
# "-[a-z][OPTIONAL]"
-
# "-[a-z]"
-
#
-
# [Argument style and description:]
-
# Instead of specifying mandatory or optional arguments directly in the
-
# switch parameter, this separate parameter can be used.
-
# "=MANDATORY"
-
# "=[OPTIONAL]"
-
#
-
# [Description:]
-
# Description string for the option.
-
# "Run verbosely"
-
# If you give multiple description strings, each string will be printed
-
# line by line.
-
#
-
# [Handler:]
-
# Handler for the parsed argument value. Either give a block or pass a
-
# Proc or Method as an argument.
-
#
-
1
def make_switch(opts, block = nil)
-
7
short, long, nolong, style, pattern, conv, not_pattern, not_conv, not_style = [], [], []
-
7
ldesc, sdesc, desc, arg = [], [], []
-
7
default_style = Switch::NoArgument
-
7
default_pattern = nil
-
7
klass = nil
-
7
q, a = nil
-
7
has_arg = false
-
-
7
opts.each do |o|
-
# argument class
-
21
next if search(:atype, o) do |pat, c|
-
1
klass = notwice(o, klass, 'type')
-
1
if not_style and not_style != Switch::NoArgument
-
not_pattern, not_conv = pat, c
-
else
-
1
default_pattern, conv = pat, c
-
end
-
end
-
-
# directly specified pattern(any object possible to match)
-
20
if (!(String === o || Symbol === o)) and o.respond_to?(:match)
-
pattern = notwice(o, pattern, 'pattern')
-
if pattern.respond_to?(:convert)
-
conv = pattern.method(:convert).to_proc
-
else
-
conv = SPLAT_PROC
-
end
-
next
-
end
-
-
# anything others
-
20
case o
-
when Proc, Method
-
block = notwice(o, block, 'block')
-
when Array, Hash
-
case pattern
-
when CompletingHash
-
when nil
-
pattern = CompletingHash.new
-
conv = pattern.method(:convert).to_proc if pattern.respond_to?(:convert)
-
else
-
raise ArgumentError, "argument pattern given twice"
-
end
-
o.each {|pat, *v| pattern[pat] = v.fetch(0) {pat}}
-
when Module
-
raise ArgumentError, "unsupported argument type: #{o}", ParseError.filter_backtrace(caller(4))
-
when *ArgumentStyle.keys
-
style = notwice(ArgumentStyle[o], style, 'style')
-
when /^--no-([^\[\]=\s]*)(.+)?/
-
1
q, a = $1, $2
-
1
o = notwice(a ? Object : TrueClass, klass, 'type')
-
1
not_pattern, not_conv = search(:atype, o) unless not_style
-
1
not_style = (not_style || default_style).guess(arg = a) if a
-
1
default_style = Switch::NoArgument
-
1
default_pattern, conv = search(:atype, FalseClass) unless default_pattern
-
1
ldesc << "--no-#{q}"
-
1
(q = q.downcase).tr!('_', '-')
-
1
long << "no-#{q}"
-
1
nolong << q
-
when /^--\[no-\]([^\[\]=\s]*)(.+)?/
-
q, a = $1, $2
-
o = notwice(a ? Object : TrueClass, klass, 'type')
-
if a
-
default_style = default_style.guess(arg = a)
-
default_pattern, conv = search(:atype, o) unless default_pattern
-
end
-
ldesc << "--[no-]#{q}"
-
(o = q.downcase).tr!('_', '-')
-
long << o
-
not_pattern, not_conv = search(:atype, FalseClass) unless not_style
-
not_style = Switch::NoArgument
-
nolong << "no-#{o}"
-
when /^--([^\[\]=\s]*)(.+)?/
-
6
q, a = $1, $2
-
6
if a
-
3
o = notwice(NilClass, klass, 'type')
-
3
default_style = default_style.guess(arg = a)
-
3
default_pattern, conv = search(:atype, o) unless default_pattern
-
end
-
6
ldesc << "--#{q}"
-
6
(o = q.downcase).tr!('_', '-')
-
6
long << o
-
when /^-(\[\^?\]?(?:[^\\\]]|\\.)*\])(.+)?/
-
q, a = $1, $2
-
o = notwice(Object, klass, 'type')
-
if a
-
default_style = default_style.guess(arg = a)
-
default_pattern, conv = search(:atype, o) unless default_pattern
-
else
-
has_arg = true
-
end
-
sdesc << "-#{q}"
-
short << Regexp.new(q)
-
when /^-(.)(.+)?/
-
6
q, a = $1, $2
-
6
if a
-
o = notwice(NilClass, klass, 'type')
-
default_style = default_style.guess(arg = a)
-
default_pattern, conv = search(:atype, o) unless default_pattern
-
end
-
6
sdesc << "-#{q}"
-
6
short << q
-
when /^=/
-
style = notwice(default_style.guess(arg = o), style, 'style')
-
default_pattern, conv = search(:atype, Object) unless default_pattern
-
else
-
7
desc.push(o)
-
end
-
end
-
-
7
default_pattern, conv = search(:atype, default_style.pattern) unless default_pattern
-
7
if !(short.empty? and long.empty?)
-
7
if has_arg and default_style == Switch::NoArgument
-
default_style = Switch::RequiredArgument
-
end
-
7
s = (style || default_style).new(pattern || default_pattern,
-
conv, sdesc, ldesc, arg, desc, block)
-
elsif !block
-
if style or pattern
-
raise ArgumentError, "no switch given", ParseError.filter_backtrace(caller)
-
end
-
s = desc
-
else
-
short << pattern
-
s = (style || default_style).new(pattern,
-
conv, nil, nil, arg, desc, block)
-
end
-
7
return s, short, long,
-
7
(not_style.new(not_pattern, not_conv, sdesc, ldesc, nil, desc, block) if not_style),
-
nolong
-
end
-
-
1
def define(*opts, &block)
-
7
top.append(*(sw = make_switch(opts, block)))
-
7
sw[0]
-
end
-
-
#
-
# Add option switch and handler. See #make_switch for an explanation of
-
# parameters.
-
#
-
1
def on(*opts, &block)
-
7
define(*opts, &block)
-
7
self
-
end
-
1
alias def_option define
-
-
1
def define_head(*opts, &block)
-
top.prepend(*(sw = make_switch(opts, block)))
-
sw[0]
-
end
-
-
#
-
# Add option switch like with #on, but at head of summary.
-
#
-
1
def on_head(*opts, &block)
-
define_head(*opts, &block)
-
self
-
end
-
1
alias def_head_option define_head
-
-
1
def define_tail(*opts, &block)
-
base.append(*(sw = make_switch(opts, block)))
-
sw[0]
-
end
-
-
#
-
# Add option switch like with #on, but at tail of summary.
-
#
-
1
def on_tail(*opts, &block)
-
define_tail(*opts, &block)
-
self
-
end
-
1
alias def_tail_option define_tail
-
-
#
-
# Add separator in summary.
-
#
-
1
def separator(string)
-
2
top.append(string, nil, nil)
-
end
-
-
#
-
# Parses command line arguments +argv+ in order. When a block is given,
-
# each non-option argument is yielded. When optional +into+ keyword
-
# argument is provided, the parsed option values are stored there via
-
# <code>[]=</code> method (so it can be Hash, or OpenStruct, or other
-
# similar object).
-
#
-
# Returns the rest of +argv+ left unparsed.
-
#
-
1
def order(*argv, into: nil, &nonopt)
-
argv = argv[0].dup if argv.size == 1 and Array === argv[0]
-
order!(argv, into: into, &nonopt)
-
end
-
-
#
-
# Same as #order, but removes switches destructively.
-
# Non-option arguments remain in +argv+.
-
#
-
1
def order!(argv = default_argv, into: nil, &nonopt)
-
1
setter = ->(name, val) {into[name.to_sym] = val} if into
-
1
parse_in_order(argv, setter, &nonopt)
-
end
-
-
1
def parse_in_order(argv = default_argv, setter = nil, &nonopt) # :nodoc:
-
1
opt, arg, val, rest = nil
-
1
nonopt ||= proc {|a| throw :terminate, a}
-
1
argv.unshift(arg) if arg = catch(:terminate) {
-
1
while arg = argv.shift
-
case arg
-
# long option
-
when /\A--([^=]*)(?:=(.*))?/m
-
opt, rest = $1, $2
-
opt.tr!('_', '-')
-
begin
-
sw, = complete(:long, opt, true)
-
rescue ParseError
-
raise $!.set_option(arg, true)
-
end
-
begin
-
opt, cb, val = sw.parse(rest, argv) {|*exc| raise(*exc)}
-
val = cb.call(val) if cb
-
setter.call(sw.switch_name, val) if setter
-
rescue ParseError
-
raise $!.set_option(arg, rest)
-
end
-
-
# short option
-
when /\A-(.)((=).*|.+)?/m
-
eq, rest, opt = $3, $2, $1
-
has_arg, val = eq, rest
-
begin
-
sw, = search(:short, opt)
-
unless sw
-
begin
-
sw, = complete(:short, opt)
-
# short option matched.
-
val = arg.delete_prefix('-')
-
has_arg = true
-
rescue InvalidOption
-
# if no short options match, try completion with long
-
# options.
-
sw, = complete(:long, opt)
-
eq ||= !rest
-
end
-
end
-
rescue ParseError
-
raise $!.set_option(arg, true)
-
end
-
begin
-
opt, cb, val = sw.parse(val, argv) {|*exc| raise(*exc) if eq}
-
raise InvalidOption, arg if has_arg and !eq and arg == "-#{opt}"
-
argv.unshift(opt) if opt and (!rest or (opt = opt.sub(/\A-*/, '-')) != '-')
-
val = cb.call(val) if cb
-
setter.call(sw.switch_name, val) if setter
-
rescue ParseError
-
raise $!.set_option(arg, arg.length > 2)
-
end
-
-
# non-option argument
-
else
-
catch(:prune) do
-
visit(:each_option) do |sw0|
-
sw = sw0
-
sw.block.call(arg) if Switch === sw and sw.match_nonswitch?(arg)
-
end
-
nonopt.call(arg)
-
end
-
end
-
end
-
-
1
nil
-
}
-
-
1
visit(:search, :short, nil) {|sw| sw.block.call(*argv) if !sw.pattern}
-
-
1
argv
-
end
-
1
private :parse_in_order
-
-
#
-
# Parses command line arguments +argv+ in permutation mode and returns
-
# list of non-option arguments. When optional +into+ keyword
-
# argument is provided, the parsed option values are stored there via
-
# <code>[]=</code> method (so it can be Hash, or OpenStruct, or other
-
# similar object).
-
#
-
1
def permute(*argv, into: nil)
-
argv = argv[0].dup if argv.size == 1 and Array === argv[0]
-
permute!(argv, into: into)
-
end
-
-
#
-
# Same as #permute, but removes switches destructively.
-
# Non-option arguments remain in +argv+.
-
#
-
1
def permute!(argv = default_argv, into: nil)
-
1
nonopts = []
-
1
order!(argv, into: into, &nonopts.method(:<<))
-
1
argv[0, 0] = nonopts
-
1
argv
-
end
-
-
#
-
# Parses command line arguments +argv+ in order when environment variable
-
# POSIXLY_CORRECT is set, and in permutation mode otherwise.
-
# When optional +into+ keyword argument is provided, the parsed option
-
# values are stored there via <code>[]=</code> method (so it can be Hash,
-
# or OpenStruct, or other similar object).
-
#
-
1
def parse(*argv, into: nil)
-
argv = argv[0].dup if argv.size == 1 and Array === argv[0]
-
parse!(argv, into: into)
-
end
-
-
#
-
# Same as #parse, but removes switches destructively.
-
# Non-option arguments remain in +argv+.
-
#
-
1
def parse!(argv = default_argv, into: nil)
-
1
if ENV.include?('POSIXLY_CORRECT')
-
order!(argv, into: into)
-
else
-
1
permute!(argv, into: into)
-
end
-
end
-
-
#
-
# Wrapper method for getopts.rb.
-
#
-
# params = ARGV.getopts("ab:", "foo", "bar:", "zot:Z;zot option")
-
# # params["a"] = true # -a
-
# # params["b"] = "1" # -b1
-
# # params["foo"] = "1" # --foo
-
# # params["bar"] = "x" # --bar x
-
# # params["zot"] = "z" # --zot Z
-
#
-
1
def getopts(*args)
-
argv = Array === args.first ? args.shift : default_argv
-
single_options, *long_options = *args
-
-
result = {}
-
-
single_options.scan(/(.)(:)?/) do |opt, val|
-
if val
-
result[opt] = nil
-
define("-#{opt} VAL")
-
else
-
result[opt] = false
-
define("-#{opt}")
-
end
-
end if single_options
-
-
long_options.each do |arg|
-
arg, desc = arg.split(';', 2)
-
opt, val = arg.split(':', 2)
-
if val
-
result[opt] = val.empty? ? nil : val
-
define("--#{opt}=#{result[opt] || "VAL"}", *[desc].compact)
-
else
-
result[opt] = false
-
define("--#{opt}", *[desc].compact)
-
end
-
end
-
-
parse_in_order(argv, result.method(:[]=))
-
result
-
end
-
-
#
-
# See #getopts.
-
#
-
1
def self.getopts(*args)
-
new.getopts(*args)
-
end
-
-
#
-
# Traverses @stack, sending each element method +id+ with +args+ and
-
# +block+.
-
#
-
1
def visit(id, *args, &block)
-
30
@stack.reverse_each do |el|
-
90
el.send(id, *args, &block)
-
end
-
nil
-
end
-
1
private :visit
-
-
#
-
# Searches +key+ in @stack for +id+ hash and returns or yields the result.
-
#
-
1
def search(id, key)
-
29
block_given = block_given?
-
29
visit(:search, id, key) do |k|
-
9
return block_given ? yield(k) : k
-
end
-
end
-
1
private :search
-
-
#
-
# Completes shortened long style option switch and returns pair of
-
# canonical switch and switch descriptor OptionParser::Switch.
-
#
-
# +typ+:: Searching table.
-
# +opt+:: Searching key.
-
# +icase+:: Search case insensitive if true.
-
# +pat+:: Optional pattern for completion.
-
#
-
1
def complete(typ, opt, icase = false, *pat)
-
if pat.empty?
-
search(typ, opt) {|sw| return [sw, opt]} # exact match or...
-
end
-
ambiguous = catch(:ambiguous) {
-
visit(:complete, typ, opt, icase, *pat) {|o, *sw| return sw}
-
}
-
exc = ambiguous ? AmbiguousOption : InvalidOption
-
raise exc.new(opt, additional: self.method(:additional_message).curry[typ])
-
end
-
1
private :complete
-
-
#
-
# Returns additional info.
-
#
-
1
def additional_message(typ, opt)
-
return unless typ and opt and defined?(DidYouMean::SpellChecker)
-
all_candidates = []
-
visit(:get_candidates, typ) do |candidates|
-
all_candidates.concat(candidates)
-
end
-
all_candidates.select! {|cand| cand.is_a?(String) }
-
checker = DidYouMean::SpellChecker.new(dictionary: all_candidates)
-
DidYouMean.formatter.message_for(all_candidates & checker.correct(opt))
-
end
-
-
1
def candidate(word)
-
list = []
-
case word
-
when '-'
-
long = short = true
-
when /\A--/
-
word, arg = word.split(/=/, 2)
-
argpat = Completion.regexp(arg, false) if arg and !arg.empty?
-
long = true
-
when /\A-/
-
short = true
-
end
-
pat = Completion.regexp(word, long)
-
visit(:each_option) do |opt|
-
next unless Switch === opt
-
opts = (long ? opt.long : []) + (short ? opt.short : [])
-
opts = Completion.candidate(word, true, pat, &opts.method(:each)).map(&:first) if pat
-
if /\A=/ =~ opt.arg
-
opts.map! {|sw| sw + "="}
-
if arg and CompletingHash === opt.pattern
-
if opts = opt.pattern.candidate(arg, false, argpat)
-
opts.map!(&:last)
-
end
-
end
-
end
-
list.concat(opts)
-
end
-
list
-
end
-
-
#
-
# Loads options from file names as +filename+. Does nothing when the file
-
# is not present. Returns whether successfully loaded.
-
#
-
# +filename+ defaults to basename of the program without suffix in a
-
# directory ~/.options, then the basename with '.options' suffix
-
# under XDG and Haiku standard places.
-
#
-
1
def load(filename = nil)
-
unless filename
-
basename = File.basename($0, '.*')
-
return true if load(File.expand_path(basename, '~/.options')) rescue nil
-
basename << ".options"
-
return [
-
# XDG
-
ENV['XDG_CONFIG_HOME'],
-
'~/.config',
-
*ENV['XDG_CONFIG_DIRS']&.split(File::PATH_SEPARATOR),
-
-
# Haiku
-
'~/config/settings',
-
].any? {|dir|
-
next if !dir or dir.empty?
-
load(File.expand_path(basename, dir)) rescue nil
-
}
-
end
-
begin
-
parse(*IO.readlines(filename).each {|s| s.chomp!})
-
true
-
rescue Errno::ENOENT, Errno::ENOTDIR
-
false
-
end
-
end
-
-
#
-
# Parses environment variable +env+ or its uppercase with splitting like a
-
# shell.
-
#
-
# +env+ defaults to the basename of the program.
-
#
-
1
def environment(env = File.basename($0, '.*'))
-
env = ENV[env] || ENV[env.upcase] or return
-
require 'shellwords'
-
parse(*Shellwords.shellwords(env))
-
end
-
-
#
-
# Acceptable argument classes
-
#
-
-
#
-
# Any string and no conversion. This is fall-back.
-
#
-
1
accept(Object) {|s,|s or s.nil?}
-
-
1
accept(NilClass) {|s,|s}
-
-
#
-
# Any non-empty string, and no conversion.
-
#
-
1
accept(String, /.+/m) {|s,*|s}
-
-
#
-
# Ruby/C-like integer, octal for 0-7 sequence, binary for 0b, hexadecimal
-
# for 0x, and decimal for others; with optional sign prefix. Converts to
-
# Integer.
-
#
-
1
decimal = '\d+(?:_\d+)*'
-
1
binary = 'b[01]+(?:_[01]+)*'
-
1
hex = 'x[\da-f]+(?:_[\da-f]+)*'
-
1
octal = "0(?:[0-7]+(?:_[0-7]+)*|#{binary}|#{hex})?"
-
1
integer = "#{octal}|#{decimal}"
-
-
1
accept(Integer, %r"\A[-+]?(?:#{integer})\z"io) {|s,|
-
begin
-
Integer(s)
-
rescue ArgumentError
-
raise OptionParser::InvalidArgument, s
-
end if s
-
}
-
-
#
-
# Float number format, and converts to Float.
-
#
-
1
float = "(?:#{decimal}(?=(.)?)(?:\\.(?:#{decimal})?)?|\\.#{decimal})(?:E[-+]?#{decimal})?"
-
1
floatpat = %r"\A[-+]?#{float}\z"io
-
1
accept(Float, floatpat) {|s,| s.to_f if s}
-
-
#
-
# Generic numeric format, converts to Integer for integer format, Float
-
# for float format, and Rational for rational format.
-
#
-
1
real = "[-+]?(?:#{octal}|#{float})"
-
1
accept(Numeric, /\A(#{real})(?:\/(#{real}))?\z/io) {|s, d, f, n,|
-
if n
-
Rational(d, n)
-
elsif f
-
Float(s)
-
else
-
Integer(s)
-
end
-
}
-
-
#
-
# Decimal integer format, to be converted to Integer.
-
#
-
1
DecimalInteger = /\A[-+]?#{decimal}\z/io
-
1
accept(DecimalInteger, DecimalInteger) {|s,|
-
begin
-
Integer(s, 10)
-
rescue ArgumentError
-
raise OptionParser::InvalidArgument, s
-
end if s
-
}
-
-
#
-
# Ruby/C like octal/hexadecimal/binary integer format, to be converted to
-
# Integer.
-
#
-
1
OctalInteger = /\A[-+]?(?:[0-7]+(?:_[0-7]+)*|0(?:#{binary}|#{hex}))\z/io
-
1
accept(OctalInteger, OctalInteger) {|s,|
-
begin
-
Integer(s, 8)
-
rescue ArgumentError
-
raise OptionParser::InvalidArgument, s
-
end if s
-
}
-
-
#
-
# Decimal integer/float number format, to be converted to Integer for
-
# integer format, Float for float format.
-
#
-
1
DecimalNumeric = floatpat # decimal integer is allowed as float also.
-
1
accept(DecimalNumeric, floatpat) {|s, f|
-
begin
-
if f
-
Float(s)
-
else
-
Integer(s)
-
end
-
rescue ArgumentError
-
raise OptionParser::InvalidArgument, s
-
end if s
-
}
-
-
#
-
# Boolean switch, which means whether it is present or not, whether it is
-
# absent or not with prefix no-, or it takes an argument
-
# yes/no/true/false/+/-.
-
#
-
1
yesno = CompletingHash.new
-
4
%w[- no false].each {|el| yesno[el] = false}
-
4
%w[+ yes true].each {|el| yesno[el] = true}
-
1
yesno['nil'] = false # should be nil?
-
1
accept(TrueClass, yesno) {|arg, val| val == nil or val}
-
#
-
# Similar to TrueClass, but defaults to false.
-
#
-
1
accept(FalseClass, yesno) {|arg, val| val != nil and val}
-
-
#
-
# List of strings separated by ",".
-
#
-
1
accept(Array) do |s, |
-
if s
-
s = s.split(',').collect {|ss| ss unless ss.empty?}
-
end
-
s
-
end
-
-
#
-
# Regular expression with options.
-
#
-
1
accept(Regexp, %r"\A/((?:\\.|[^\\])*)/([[:alpha:]]+)?\z|.*") do |all, s, o|
-
f = 0
-
if o
-
f |= Regexp::IGNORECASE if /i/ =~ o
-
f |= Regexp::MULTILINE if /m/ =~ o
-
f |= Regexp::EXTENDED if /x/ =~ o
-
k = o.delete("imx")
-
k = nil if k.empty?
-
end
-
Regexp.new(s || all, f, k)
-
end
-
-
#
-
# Exceptions
-
#
-
-
#
-
# Base class of exceptions from OptionParser.
-
#
-
1
class ParseError < RuntimeError
-
# Reason which caused the error.
-
1
Reason = 'parse error'
-
-
1
def initialize(*args, additional: nil)
-
@additional = additional
-
@arg0, = args
-
@args = args
-
@reason = nil
-
end
-
-
1
attr_reader :args
-
1
attr_writer :reason
-
1
attr_accessor :additional
-
-
#
-
# Pushes back erred argument(s) to +argv+.
-
#
-
1
def recover(argv)
-
argv[0, 0] = @args
-
argv
-
end
-
-
1
def self.filter_backtrace(array)
-
unless $DEBUG
-
array.delete_if(&%r"\A#{Regexp.quote(__FILE__)}:"o.method(:=~))
-
end
-
array
-
end
-
-
1
def set_backtrace(array)
-
super(self.class.filter_backtrace(array))
-
end
-
-
1
def set_option(opt, eq)
-
if eq
-
@args[0] = opt
-
else
-
@args.unshift(opt)
-
end
-
self
-
end
-
-
#
-
# Returns error reason. Override this for I18N.
-
#
-
1
def reason
-
@reason || self.class::Reason
-
end
-
-
1
def inspect
-
"#<#{self.class}: #{args.join(' ')}>"
-
end
-
-
#
-
# Default stringizing method to emit standard error message.
-
#
-
1
def message
-
"#{reason}: #{args.join(' ')}#{additional[@arg0] if additional}"
-
end
-
-
1
alias to_s message
-
end
-
-
#
-
# Raises when ambiguously completable string is encountered.
-
#
-
1
class AmbiguousOption < ParseError
-
1
const_set(:Reason, 'ambiguous option')
-
end
-
-
#
-
# Raises when there is an argument for a switch which takes no argument.
-
#
-
1
class NeedlessArgument < ParseError
-
1
const_set(:Reason, 'needless argument')
-
end
-
-
#
-
# Raises when a switch with mandatory argument has no argument.
-
#
-
1
class MissingArgument < ParseError
-
1
const_set(:Reason, 'missing argument')
-
end
-
-
#
-
# Raises when switch is undefined.
-
#
-
1
class InvalidOption < ParseError
-
1
const_set(:Reason, 'invalid option')
-
end
-
-
#
-
# Raises when the given argument does not match required format.
-
#
-
1
class InvalidArgument < ParseError
-
1
const_set(:Reason, 'invalid argument')
-
end
-
-
#
-
# Raises when the given argument word can't be completed uniquely.
-
#
-
1
class AmbiguousArgument < InvalidArgument
-
1
const_set(:Reason, 'ambiguous argument')
-
end
-
-
#
-
# Miscellaneous
-
#
-
-
#
-
# Extends command line arguments array (ARGV) to parse itself.
-
#
-
1
module Arguable
-
-
#
-
# Sets OptionParser object, when +opt+ is +false+ or +nil+, methods
-
# OptionParser::Arguable#options and OptionParser::Arguable#options= are
-
# undefined. Thus, there is no ways to access the OptionParser object
-
# via the receiver object.
-
#
-
1
def options=(opt)
-
unless @optparse = opt
-
class << self
-
undef_method(:options)
-
undef_method(:options=)
-
end
-
end
-
end
-
-
#
-
# Actual OptionParser object, automatically created if nonexistent.
-
#
-
# If called with a block, yields the OptionParser object and returns the
-
# result of the block. If an OptionParser::ParseError exception occurs
-
# in the block, it is rescued, a error message printed to STDERR and
-
# +nil+ returned.
-
#
-
1
def options
-
@optparse ||= OptionParser.new
-
@optparse.default_argv = self
-
block_given? or return @optparse
-
begin
-
yield @optparse
-
rescue ParseError
-
@optparse.warn $!
-
nil
-
end
-
end
-
-
#
-
# Parses +self+ destructively in order and returns +self+ containing the
-
# rest arguments left unparsed.
-
#
-
1
def order!(&blk) options.order!(self, &blk) end
-
-
#
-
# Parses +self+ destructively in permutation mode and returns +self+
-
# containing the rest arguments left unparsed.
-
#
-
1
def permute!() options.permute!(self) end
-
-
#
-
# Parses +self+ destructively and returns +self+ containing the
-
# rest arguments left unparsed.
-
#
-
1
def parse!() options.parse!(self) end
-
-
#
-
# Substitution of getopts is possible as follows. Also see
-
# OptionParser#getopts.
-
#
-
# def getopts(*args)
-
# ($OPT = ARGV.getopts(*args)).each do |opt, val|
-
# eval "$OPT_#{opt.gsub(/[^A-Za-z0-9_]/, '_')} = val"
-
# end
-
# rescue OptionParser::ParseError
-
# end
-
#
-
1
def getopts(*args)
-
options.getopts(self, *args)
-
end
-
-
#
-
# Initializes instance variable.
-
#
-
1
def self.extend_object(obj)
-
1
super
-
2
obj.instance_eval {@optparse = nil}
-
end
-
1
def initialize(*args)
-
super
-
@optparse = nil
-
end
-
end
-
-
#
-
# Acceptable argument classes. Now contains DecimalInteger, OctalInteger
-
# and DecimalNumeric. See Acceptable argument classes (in source code).
-
#
-
1
module Acceptables
-
1
const_set(:DecimalInteger, OptionParser::DecimalInteger)
-
1
const_set(:OctalInteger, OptionParser::OctalInteger)
-
1
const_set(:DecimalNumeric, OptionParser::DecimalNumeric)
-
end
-
end
-
-
# ARGV is arguable by OptionParser
-
1
ARGV.extend(OptionParser::Arguable)
-
-
# An alias for OptionParser.
-
1
OptParse = OptionParser # :nodoc:
-
# -*- coding: utf-8 -*-
-
# frozen_string_literal: true
-
#--
-
# Copyright (C) 2004 Mauricio Julio Fernández Pradier
-
# See LICENSE.txt for additional licensing information.
-
#++
-
#
-
# Example using a Gem::Package
-
#
-
# Builds a .gem file given a Gem::Specification. A .gem file is a tarball
-
# which contains a data.tar.gz and metadata.gz, and possibly signatures.
-
#
-
# require 'rubygems'
-
# require 'rubygems/package'
-
#
-
# spec = Gem::Specification.new do |s|
-
# s.summary = "Ruby based make-like utility."
-
# s.name = 'rake'
-
# s.version = PKG_VERSION
-
# s.requirements << 'none'
-
# s.files = PKG_FILES
-
# s.description = <<-EOF
-
# Rake is a Make-like program implemented in Ruby. Tasks
-
# and dependencies are specified in standard Ruby syntax.
-
# EOF
-
# end
-
#
-
# Gem::Package.build spec
-
#
-
# Reads a .gem file.
-
#
-
# require 'rubygems'
-
# require 'rubygems/package'
-
#
-
# the_gem = Gem::Package.new(path_to_dot_gem)
-
# the_gem.contents # get the files in the gem
-
# the_gem.extract_files destination_directory # extract the gem into a directory
-
# the_gem.spec # get the spec out of the gem
-
# the_gem.verify # check the gem is OK (contains valid gem specification, contains a not corrupt contents archive)
-
#
-
# #files are the files in the .gem tar file, not the Ruby files in the gem
-
# #extract_files and #contents automatically call #verify
-
-
1
require 'rubygems/security'
-
1
require 'rubygems/specification'
-
1
require 'rubygems/user_interaction'
-
1
require 'zlib'
-
-
1
class Gem::Package
-
-
1
include Gem::UserInteraction
-
-
1
class Error < Gem::Exception; end
-
-
1
class FormatError < Error
-
-
1
attr_reader :path
-
-
1
def initialize(message, source = nil)
-
if source
-
@path = source.path
-
-
message = message + " in #{path}" if path
-
end
-
-
super message
-
end
-
-
end
-
-
1
class PathError < Error
-
-
1
def initialize(destination, destination_dir)
-
super "installing into parent path %s of %s is not allowed" %
-
[destination, destination_dir]
-
end
-
-
end
-
-
1
class NonSeekableIO < Error; end
-
-
1
class TooLongFileName < Error; end
-
-
##
-
# Raised when a tar file is corrupt
-
-
1
class TarInvalidError < Error; end
-
-
1
attr_accessor :build_time # :nodoc:
-
-
##
-
# Checksums for the contents of the package
-
-
1
attr_reader :checksums
-
-
##
-
# The files in this package. This is not the contents of the gem, just the
-
# files in the top-level container.
-
-
1
attr_reader :files
-
-
##
-
# Reference to the gem being packaged.
-
-
1
attr_reader :gem
-
-
##
-
# The security policy used for verifying the contents of this package.
-
-
1
attr_accessor :security_policy
-
-
##
-
# Sets the Gem::Specification to use to build this package.
-
-
1
attr_writer :spec
-
-
##
-
# Permission for directories
-
1
attr_accessor :dir_mode
-
-
##
-
# Permission for program files
-
1
attr_accessor :prog_mode
-
-
##
-
# Permission for other files
-
1
attr_accessor :data_mode
-
-
1
def self.build(spec, skip_validation = false, strict_validation = false, file_name = nil)
-
gem_file = file_name || spec.file_name
-
-
package = new gem_file
-
package.spec = spec
-
package.build skip_validation, strict_validation
-
-
gem_file
-
end
-
-
##
-
# Creates a new Gem::Package for the file at +gem+. +gem+ can also be
-
# provided as an IO object.
-
#
-
# If +gem+ is an existing file in the old format a Gem::Package::Old will be
-
# returned.
-
-
1
def self.new(gem, security_policy = nil)
-
gem = if gem.is_a?(Gem::Package::Source)
-
gem
-
elsif gem.respond_to? :read
-
Gem::Package::IOSource.new gem
-
else
-
Gem::Package::FileSource.new gem
-
end
-
-
return super unless Gem::Package == self
-
return super unless gem.present?
-
-
return super unless gem.start
-
return super unless gem.start.include? 'MD5SUM ='
-
-
Gem::Package::Old.new gem
-
end
-
-
##
-
# Extracts the Gem::Specification and raw metadata from the .gem file at
-
# +path+.
-
#--
-
-
1
def self.raw_spec(path, security_policy = nil)
-
format = new(path, security_policy)
-
spec = format.spec
-
-
metadata = nil
-
-
File.open path, Gem.binary_mode do |io|
-
tar = Gem::Package::TarReader.new io
-
tar.each_entry do |entry|
-
case entry.full_name
-
when 'metadata' then
-
metadata = entry.read
-
when 'metadata.gz' then
-
metadata = Gem::Util.gunzip entry.read
-
end
-
end
-
end
-
-
return spec, metadata
-
end
-
-
##
-
# Creates a new package that will read or write to the file +gem+.
-
-
1
def initialize(gem, security_policy) # :notnew:
-
@gem = gem
-
-
@build_time = Gem.source_date_epoch
-
@checksums = {}
-
@contents = nil
-
@digests = Hash.new { |h, algorithm| h[algorithm] = {} }
-
@files = nil
-
@security_policy = security_policy
-
@signatures = {}
-
@signer = nil
-
@spec = nil
-
end
-
-
##
-
# Copies this package to +path+ (if possible)
-
-
1
def copy_to(path)
-
FileUtils.cp @gem.path, path unless File.exist? path
-
end
-
-
##
-
# Adds a checksum for each entry in the gem to checksums.yaml.gz.
-
-
1
def add_checksums(tar)
-
Gem.load_yaml
-
-
checksums_by_algorithm = Hash.new { |h, algorithm| h[algorithm] = {} }
-
-
@checksums.each do |name, digests|
-
digests.each do |algorithm, digest|
-
checksums_by_algorithm[algorithm][name] = digest.hexdigest
-
end
-
end
-
-
tar.add_file_signed 'checksums.yaml.gz', 0444, @signer do |io|
-
gzip_to io do |gz_io|
-
YAML.dump checksums_by_algorithm, gz_io
-
end
-
end
-
end
-
-
##
-
# Adds the files listed in the packages's Gem::Specification to data.tar.gz
-
# and adds this file to the +tar+.
-
-
1
def add_contents(tar) # :nodoc:
-
digests = tar.add_file_signed 'data.tar.gz', 0444, @signer do |io|
-
gzip_to io do |gz_io|
-
Gem::Package::TarWriter.new gz_io do |data_tar|
-
add_files data_tar
-
end
-
end
-
end
-
-
@checksums['data.tar.gz'] = digests
-
end
-
-
##
-
# Adds files included the package's Gem::Specification to the +tar+ file
-
-
1
def add_files(tar) # :nodoc:
-
@spec.files.each do |file|
-
stat = File.lstat file
-
-
if stat.symlink?
-
target_path = File.readlink(file)
-
-
unless target_path.start_with? '.'
-
relative_dir = File.dirname(file).sub("#{Dir.pwd}/", '')
-
target_path = File.join(relative_dir, target_path)
-
end
-
-
tar.add_symlink file, target_path, stat.mode
-
end
-
-
next unless stat.file?
-
-
tar.add_file_simple file, stat.mode, stat.size do |dst_io|
-
File.open file, 'rb' do |src_io|
-
dst_io.write src_io.read 16384 until src_io.eof?
-
end
-
end
-
end
-
end
-
-
##
-
# Adds the package's Gem::Specification to the +tar+ file
-
-
1
def add_metadata(tar) # :nodoc:
-
digests = tar.add_file_signed 'metadata.gz', 0444, @signer do |io|
-
gzip_to io do |gz_io|
-
gz_io.write @spec.to_yaml
-
end
-
end
-
-
@checksums['metadata.gz'] = digests
-
end
-
-
##
-
# Builds this package based on the specification set by #spec=
-
-
1
def build(skip_validation = false, strict_validation = false)
-
raise ArgumentError, "skip_validation = true and strict_validation = true are incompatible" if skip_validation && strict_validation
-
-
Gem.load_yaml
-
-
@spec.mark_version
-
@spec.validate true, strict_validation unless skip_validation
-
-
setup_signer(
-
signer_options: {
-
expiration_length_days: Gem.configuration.cert_expiration_length_days
-
}
-
)
-
-
@gem.with_write_io do |gem_io|
-
Gem::Package::TarWriter.new gem_io do |gem|
-
add_metadata gem
-
add_contents gem
-
add_checksums gem
-
end
-
end
-
-
say <<-EOM
-
Successfully built RubyGem
-
Name: #{@spec.name}
-
Version: #{@spec.version}
-
File: #{File.basename @gem.path}
-
EOM
-
ensure
-
@signer = nil
-
end
-
-
##
-
# A list of file names contained in this gem
-
-
1
def contents
-
return @contents if @contents
-
-
verify unless @spec
-
-
@contents = []
-
-
@gem.with_read_io do |io|
-
gem_tar = Gem::Package::TarReader.new io
-
-
gem_tar.each do |entry|
-
next unless entry.full_name == 'data.tar.gz'
-
-
open_tar_gz entry do |pkg_tar|
-
pkg_tar.each do |contents_entry|
-
@contents << contents_entry.full_name
-
end
-
end
-
-
return @contents
-
end
-
end
-
end
-
-
##
-
# Creates a digest of the TarEntry +entry+ from the digest algorithm set by
-
# the security policy.
-
-
1
def digest(entry) # :nodoc:
-
algorithms = if @checksums
-
@checksums.keys
-
else
-
[Gem::Security::DIGEST_NAME].compact
-
end
-
-
algorithms.each do |algorithm|
-
digester =
-
if defined?(OpenSSL::Digest)
-
OpenSSL::Digest.new algorithm
-
else
-
Digest.const_get(algorithm).new
-
end
-
-
digester << entry.read(16384) until entry.eof?
-
-
entry.rewind
-
-
@digests[algorithm][entry.full_name] = digester
-
end
-
-
@digests
-
end
-
-
##
-
# Extracts the files in this package into +destination_dir+
-
#
-
# If +pattern+ is specified, only entries matching that glob will be
-
# extracted.
-
-
1
def extract_files(destination_dir, pattern = "*")
-
verify unless @spec
-
-
FileUtils.mkdir_p destination_dir, :mode => dir_mode && 0755
-
-
@gem.with_read_io do |io|
-
reader = Gem::Package::TarReader.new io
-
-
reader.each do |entry|
-
next unless entry.full_name == 'data.tar.gz'
-
-
extract_tar_gz entry, destination_dir, pattern
-
-
return # ignore further entries
-
end
-
end
-
end
-
-
##
-
# Extracts all the files in the gzipped tar archive +io+ into
-
# +destination_dir+.
-
#
-
# If an entry in the archive contains a relative path above
-
# +destination_dir+ or an absolute path is encountered an exception is
-
# raised.
-
#
-
# If +pattern+ is specified, only entries matching that glob will be
-
# extracted.
-
-
1
def extract_tar_gz(io, destination_dir, pattern = "*") # :nodoc:
-
directories = [] if dir_mode
-
open_tar_gz io do |tar|
-
tar.each do |entry|
-
next unless File.fnmatch pattern, entry.full_name, File::FNM_DOTMATCH
-
-
destination = install_location entry.full_name, destination_dir
-
-
FileUtils.rm_rf destination
-
-
mkdir_options = {}
-
mkdir_options[:mode] = dir_mode ? 0755 : (entry.header.mode if entry.directory?)
-
mkdir =
-
if entry.directory?
-
destination
-
else
-
File.dirname destination
-
end
-
directories << mkdir if directories
-
-
mkdir_p_safe mkdir, mkdir_options, destination_dir, entry.full_name
-
-
File.open destination, 'wb' do |out|
-
out.write entry.read
-
FileUtils.chmod file_mode(entry.header.mode), destination
-
end if entry.file?
-
-
File.symlink(entry.header.linkname, destination) if entry.symlink?
-
-
verbose destination
-
end
-
end
-
-
if directories
-
directories.uniq!
-
File.chmod(dir_mode, *directories)
-
end
-
end
-
-
1
def file_mode(mode) # :nodoc:
-
((mode & 0111).zero? ? data_mode : prog_mode) || mode
-
end
-
-
##
-
# Gzips content written to +gz_io+ to +io+.
-
#--
-
# Also sets the gzip modification time to the package build time to ease
-
# testing.
-
-
1
def gzip_to(io) # :yields: gz_io
-
gz_io = Zlib::GzipWriter.new io, Zlib::BEST_COMPRESSION
-
gz_io.mtime = @build_time
-
-
yield gz_io
-
ensure
-
gz_io.close
-
end
-
-
##
-
# Returns the full path for installing +filename+.
-
#
-
# If +filename+ is not inside +destination_dir+ an exception is raised.
-
-
1
def install_location(filename, destination_dir) # :nodoc:
-
raise Gem::Package::PathError.new(filename, destination_dir) if
-
filename.start_with? '/'
-
-
destination_dir = File.expand_path(File.realpath(destination_dir))
-
destination = File.expand_path(File.join(destination_dir, filename))
-
-
raise Gem::Package::PathError.new(destination, destination_dir) unless
-
destination.start_with? destination_dir + '/'
-
-
begin
-
real_destination = File.expand_path(File.realpath(destination))
-
rescue
-
# it's fine if the destination doesn't exist, because rm -rf'ing it can't cause any damage
-
nil
-
else
-
raise Gem::Package::PathError.new(real_destination, destination_dir) unless
-
real_destination.start_with? destination_dir + '/'
-
end
-
-
destination.tap(&Gem::UNTAINT)
-
destination
-
end
-
-
1
def normalize_path(pathname)
-
if Gem.win_platform?
-
pathname.downcase
-
else
-
pathname
-
end
-
end
-
-
1
def mkdir_p_safe(mkdir, mkdir_options, destination_dir, file_name)
-
destination_dir = File.realpath(File.expand_path(destination_dir))
-
parts = mkdir.split(File::SEPARATOR)
-
parts.reduce do |path, basename|
-
path = File.realpath(path) unless path == ""
-
path = File.expand_path(path + File::SEPARATOR + basename)
-
lstat = File.lstat path rescue nil
-
if !lstat || !lstat.directory?
-
unless normalize_path(path).start_with? normalize_path(destination_dir) and (FileUtils.mkdir path, **mkdir_options rescue false)
-
raise Gem::Package::PathError.new(file_name, destination_dir)
-
end
-
end
-
path
-
end
-
end
-
-
##
-
# Loads a Gem::Specification from the TarEntry +entry+
-
-
1
def load_spec(entry) # :nodoc:
-
case entry.full_name
-
when 'metadata' then
-
@spec = Gem::Specification.from_yaml entry.read
-
when 'metadata.gz' then
-
Zlib::GzipReader.wrap(entry, external_encoding: Encoding::UTF_8) do |gzio|
-
@spec = Gem::Specification.from_yaml gzio.read
-
end
-
end
-
end
-
-
##
-
# Opens +io+ as a gzipped tar archive
-
-
1
def open_tar_gz(io) # :nodoc:
-
Zlib::GzipReader.wrap io do |gzio|
-
tar = Gem::Package::TarReader.new gzio
-
-
yield tar
-
end
-
end
-
-
##
-
# Reads and loads checksums.yaml.gz from the tar file +gem+
-
-
1
def read_checksums(gem)
-
Gem.load_yaml
-
-
@checksums = gem.seek 'checksums.yaml.gz' do |entry|
-
Zlib::GzipReader.wrap entry do |gz_io|
-
Gem::SafeYAML.safe_load gz_io.read
-
end
-
end
-
end
-
-
##
-
# Prepares the gem for signing and checksum generation. If a signing
-
# certificate and key are not present only checksum generation is set up.
-
-
1
def setup_signer(signer_options: {})
-
passphrase = ENV['GEM_PRIVATE_KEY_PASSPHRASE']
-
if @spec.signing_key
-
@signer =
-
Gem::Security::Signer.new(
-
@spec.signing_key,
-
@spec.cert_chain,
-
passphrase,
-
signer_options
-
)
-
-
@spec.signing_key = nil
-
@spec.cert_chain = @signer.cert_chain.map { |cert| cert.to_s }
-
else
-
@signer = Gem::Security::Signer.new nil, nil, passphrase
-
@spec.cert_chain = @signer.cert_chain.map { |cert| cert.to_pem } if
-
@signer.cert_chain
-
end
-
end
-
-
##
-
# The spec for this gem.
-
#
-
# If this is a package for a built gem the spec is loaded from the
-
# gem and returned. If this is a package for a gem being built the provided
-
# spec is returned.
-
-
1
def spec
-
verify unless @spec
-
-
@spec
-
end
-
-
##
-
# Verifies that this gem:
-
#
-
# * Contains a valid gem specification
-
# * Contains a contents archive
-
# * The contents archive is not corrupt
-
#
-
# After verification the gem specification from the gem is available from
-
# #spec
-
-
1
def verify
-
@files = []
-
@spec = nil
-
-
@gem.with_read_io do |io|
-
Gem::Package::TarReader.new io do |reader|
-
read_checksums reader
-
-
verify_files reader
-
end
-
end
-
-
verify_checksums @digests, @checksums
-
-
@security_policy.verify_signatures @spec, @digests, @signatures if
-
@security_policy
-
-
true
-
rescue Gem::Security::Exception
-
@spec = nil
-
@files = []
-
raise
-
rescue Errno::ENOENT => e
-
raise Gem::Package::FormatError.new e.message
-
rescue Gem::Package::TarInvalidError => e
-
raise Gem::Package::FormatError.new e.message, @gem
-
end
-
-
##
-
# Verifies the +checksums+ against the +digests+. This check is not
-
# cryptographically secure. Missing checksums are ignored.
-
-
1
def verify_checksums(digests, checksums) # :nodoc:
-
return unless checksums
-
-
checksums.sort.each do |algorithm, gem_digests|
-
gem_digests.sort.each do |file_name, gem_hexdigest|
-
computed_digest = digests[algorithm][file_name]
-
-
unless computed_digest.hexdigest == gem_hexdigest
-
raise Gem::Package::FormatError.new \
-
"#{algorithm} checksum mismatch for #{file_name}", @gem
-
end
-
end
-
end
-
end
-
-
##
-
# Verifies +entry+ in a .gem file.
-
-
1
def verify_entry(entry)
-
file_name = entry.full_name
-
@files << file_name
-
-
case file_name
-
when /\.sig$/ then
-
@signatures[$`] = entry.read if @security_policy
-
return
-
else
-
digest entry
-
end
-
-
case file_name
-
when "metadata", "metadata.gz" then
-
load_spec entry
-
when 'data.tar.gz' then
-
verify_gz entry
-
end
-
rescue => e
-
message = "package is corrupt, exception while verifying: " +
-
"#{e.message} (#{e.class})"
-
raise Gem::Package::FormatError.new message, @gem
-
end
-
-
##
-
# Verifies the files of the +gem+
-
-
1
def verify_files(gem)
-
gem.each do |entry|
-
verify_entry entry
-
end
-
-
unless @spec
-
raise Gem::Package::FormatError.new 'package metadata is missing', @gem
-
end
-
-
unless @files.include? 'data.tar.gz'
-
raise Gem::Package::FormatError.new \
-
'package content (data.tar.gz) is missing', @gem
-
end
-
-
if duplicates = @files.group_by {|f| f }.select {|k,v| v.size > 1 }.map(&:first) and duplicates.any?
-
raise Gem::Security::Exception, "duplicate files in the package: (#{duplicates.map(&:inspect).join(', ')})"
-
end
-
end
-
-
##
-
# Verifies that +entry+ is a valid gzipped file.
-
-
1
def verify_gz(entry) # :nodoc:
-
Zlib::GzipReader.wrap entry do |gzio|
-
gzio.read 16384 until gzio.eof? # gzip checksum verification
-
end
-
rescue Zlib::GzipFile::Error => e
-
raise Gem::Package::FormatError.new(e.message, entry.full_name)
-
end
-
-
end
-
-
1
require 'rubygems/package/digest_io'
-
1
require 'rubygems/package/source'
-
1
require 'rubygems/package/file_source'
-
1
require 'rubygems/package/io_source'
-
1
require 'rubygems/package/old'
-
1
require 'rubygems/package/tar_header'
-
1
require 'rubygems/package/tar_reader'
-
1
require 'rubygems/package/tar_reader/entry'
-
1
require 'rubygems/package/tar_writer'
-
# frozen_string_literal: true
-
##
-
# IO wrapper that creates digests of contents written to the IO it wraps.
-
-
1
class Gem::Package::DigestIO
-
-
##
-
# Collected digests for wrapped writes.
-
#
-
# {
-
# 'SHA1' => #<OpenSSL::Digest: [...]>,
-
# 'SHA512' => #<OpenSSL::Digest: [...]>,
-
# }
-
-
1
attr_reader :digests
-
-
##
-
# Wraps +io+ and updates digest for each of the digest algorithms in
-
# the +digests+ Hash. Returns the digests hash. Example:
-
#
-
# io = StringIO.new
-
# digests = {
-
# 'SHA1' => OpenSSL::Digest.new('SHA1'),
-
# 'SHA512' => OpenSSL::Digest.new('SHA512'),
-
# }
-
#
-
# Gem::Package::DigestIO.wrap io, digests do |digest_io|
-
# digest_io.write "hello"
-
# end
-
#
-
# digests['SHA1'].hexdigest #=> "aaf4c61d[...]"
-
# digests['SHA512'].hexdigest #=> "9b71d224[...]"
-
-
1
def self.wrap(io, digests)
-
digest_io = new io, digests
-
-
yield digest_io
-
-
return digests
-
end
-
-
##
-
# Creates a new DigestIO instance. Using ::wrap is recommended, see the
-
# ::wrap documentation for documentation of +io+ and +digests+.
-
-
1
def initialize(io, digests)
-
@io = io
-
@digests = digests
-
end
-
-
##
-
# Writes +data+ to the underlying IO and updates the digests
-
-
1
def write(data)
-
result = @io.write data
-
-
@digests.each do |_, digest|
-
digest << data
-
end
-
-
result
-
end
-
-
end
-
# frozen_string_literal: true
-
##
-
# The primary source of gems is a file on disk, including all usages
-
# internal to rubygems.
-
#
-
# This is a private class, do not depend on it directly. Instead, pass a path
-
# object to `Gem::Package.new`.
-
-
1
class Gem::Package::FileSource < Gem::Package::Source # :nodoc: all
-
-
1
attr_reader :path
-
-
1
def initialize(path)
-
@path = path
-
end
-
-
1
def start
-
@start ||= File.read path, 20
-
end
-
-
1
def present?
-
File.exist? path
-
end
-
-
1
def with_write_io(&block)
-
File.open path, 'wb', &block
-
end
-
-
1
def with_read_io(&block)
-
File.open path, 'rb', &block
-
end
-
-
end
-
# frozen_string_literal: true
-
##
-
# Supports reading and writing gems from/to a generic IO object. This is
-
# useful for other applications built on top of rubygems, such as
-
# rubygems.org.
-
#
-
# This is a private class, do not depend on it directly. Instead, pass an IO
-
# object to `Gem::Package.new`.
-
-
1
class Gem::Package::IOSource < Gem::Package::Source # :nodoc: all
-
-
1
attr_reader :io
-
-
1
def initialize(io)
-
@io = io
-
end
-
-
1
def start
-
@start ||= begin
-
if io.pos > 0
-
raise Gem::Package::Error, "Cannot read start unless IO is at start"
-
end
-
-
value = io.read 20
-
io.rewind
-
value
-
end
-
end
-
-
1
def present?
-
true
-
end
-
-
1
def with_read_io
-
yield io
-
end
-
-
1
def with_write_io
-
yield io
-
end
-
-
1
def path
-
end
-
-
end
-
# frozen_string_literal: true
-
#--
-
# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others.
-
# All rights reserved.
-
# See LICENSE.txt for permissions.
-
#++
-
-
##
-
# The format class knows the guts of the ancient .gem file format and provides
-
# the capability to read such ancient gems.
-
#
-
# Please pretend this doesn't exist.
-
-
1
class Gem::Package::Old < Gem::Package
-
-
1
undef_method :spec=
-
-
##
-
# Creates a new old-format package reader for +gem+. Old-format packages
-
# cannot be written.
-
-
1
def initialize(gem, security_policy)
-
require 'fileutils'
-
require 'zlib'
-
Gem.load_yaml
-
-
@contents = nil
-
@gem = gem
-
@security_policy = security_policy
-
@spec = nil
-
end
-
-
##
-
# A list of file names contained in this gem
-
-
1
def contents
-
verify
-
-
return @contents if @contents
-
-
@gem.with_read_io do |io|
-
read_until_dashes io # spec
-
header = file_list io
-
-
@contents = header.map { |file| file['path'] }
-
end
-
end
-
-
##
-
# Extracts the files in this package into +destination_dir+
-
-
1
def extract_files(destination_dir)
-
verify
-
-
errstr = "Error reading files from gem"
-
-
@gem.with_read_io do |io|
-
read_until_dashes io # spec
-
header = file_list io
-
raise Gem::Exception, errstr unless header
-
-
header.each do |entry|
-
full_name = entry['path']
-
-
destination = install_location full_name, destination_dir
-
-
file_data = String.new
-
-
read_until_dashes io do |line|
-
file_data << line
-
end
-
-
file_data = file_data.strip.unpack("m")[0]
-
file_data = Zlib::Inflate.inflate file_data
-
-
raise Gem::Package::FormatError, "#{full_name} in #{@gem} is corrupt" if
-
file_data.length != entry['size'].to_i
-
-
FileUtils.rm_rf destination
-
-
FileUtils.mkdir_p File.dirname(destination), :mode => dir_mode && 0755
-
-
File.open destination, 'wb', file_mode(entry['mode']) do |out|
-
out.write file_data
-
end
-
-
verbose destination
-
end
-
end
-
rescue Zlib::DataError
-
raise Gem::Exception, errstr
-
end
-
-
##
-
# Reads the file list section from the old-format gem +io+
-
-
1
def file_list(io) # :nodoc:
-
header = String.new
-
-
read_until_dashes io do |line|
-
header << line
-
end
-
-
Gem::SafeYAML.safe_load header
-
end
-
-
##
-
# Reads lines until a "---" separator is found
-
-
1
def read_until_dashes(io) # :nodoc:
-
while (line = io.gets) && line.chomp.strip != "---" do
-
yield line if block_given?
-
end
-
end
-
-
##
-
# Skips the Ruby self-install header in +io+.
-
-
1
def skip_ruby(io) # :nodoc:
-
loop do
-
line = io.gets
-
-
return if line.chomp == '__END__'
-
break unless line
-
end
-
-
raise Gem::Exception, "Failed to find end of Ruby script while reading gem"
-
end
-
-
##
-
# The specification for this gem
-
-
1
def spec
-
verify
-
-
return @spec if @spec
-
-
yaml = String.new
-
-
@gem.with_read_io do |io|
-
skip_ruby io
-
read_until_dashes io do |line|
-
yaml << line
-
end
-
end
-
-
begin
-
@spec = Gem::Specification.from_yaml yaml
-
rescue YAML::SyntaxError
-
raise Gem::Exception, "Failed to parse gem specification out of gem file"
-
end
-
rescue ArgumentError
-
raise Gem::Exception, "Failed to parse gem specification out of gem file"
-
end
-
-
##
-
# Raises an exception if a security policy that verifies data is active.
-
# Old format gems cannot be verified as signed.
-
-
1
def verify
-
return true unless @security_policy
-
-
raise Gem::Security::Exception,
-
'old format gems do not contain signatures and cannot be verified' if
-
@security_policy.verify_data
-
-
true
-
end
-
-
end
-
# frozen_string_literal: true
-
1
class Gem::Package::Source # :nodoc:
-
end
-
# -*- coding: utf-8 -*-
-
# frozen_string_literal: true
-
#--
-
# Copyright (C) 2004 Mauricio Julio Fernández Pradier
-
# See LICENSE.txt for additional licensing information.
-
#++
-
-
##
-
#--
-
# struct tarfile_entry_posix {
-
# char name[100]; # ASCII + (Z unless filled)
-
# char mode[8]; # 0 padded, octal, null
-
# char uid[8]; # ditto
-
# char gid[8]; # ditto
-
# char size[12]; # 0 padded, octal, null
-
# char mtime[12]; # 0 padded, octal, null
-
# char checksum[8]; # 0 padded, octal, null, space
-
# char typeflag[1]; # file: "0" dir: "5"
-
# char linkname[100]; # ASCII + (Z unless filled)
-
# char magic[6]; # "ustar\0"
-
# char version[2]; # "00"
-
# char uname[32]; # ASCIIZ
-
# char gname[32]; # ASCIIZ
-
# char devmajor[8]; # 0 padded, octal, null
-
# char devminor[8]; # o padded, octal, null
-
# char prefix[155]; # ASCII + (Z unless filled)
-
# };
-
#++
-
# A header for a tar file
-
-
1
class Gem::Package::TarHeader
-
-
##
-
# Fields in the tar header
-
-
1
FIELDS = [
-
:checksum,
-
:devmajor,
-
:devminor,
-
:gid,
-
:gname,
-
:linkname,
-
:magic,
-
:mode,
-
:mtime,
-
:name,
-
:prefix,
-
:size,
-
:typeflag,
-
:uid,
-
:uname,
-
:version,
-
].freeze
-
-
##
-
# Pack format for a tar header
-
-
1
PACK_FORMAT = 'a100' + # name
-
'a8' + # mode
-
'a8' + # uid
-
'a8' + # gid
-
'a12' + # size
-
'a12' + # mtime
-
'a7a' + # chksum
-
'a' + # typeflag
-
'a100' + # linkname
-
'a6' + # magic
-
'a2' + # version
-
'a32' + # uname
-
'a32' + # gname
-
'a8' + # devmajor
-
'a8' + # devminor
-
'a155' # prefix
-
-
##
-
# Unpack format for a tar header
-
-
1
UNPACK_FORMAT = 'A100' + # name
-
'A8' + # mode
-
'A8' + # uid
-
'A8' + # gid
-
'A12' + # size
-
'A12' + # mtime
-
'A8' + # checksum
-
'A' + # typeflag
-
'A100' + # linkname
-
'A6' + # magic
-
'A2' + # version
-
'A32' + # uname
-
'A32' + # gname
-
'A8' + # devmajor
-
'A8' + # devminor
-
'A155' # prefix
-
-
1
attr_reader(*FIELDS)
-
-
1
EMPTY_HEADER = ("\0" * 512).freeze # :nodoc:
-
-
##
-
# Creates a tar header from IO +stream+
-
-
1
def self.from(stream)
-
5
header = stream.read 512
-
5
empty = (EMPTY_HEADER == header)
-
-
5
fields = header.unpack UNPACK_FORMAT
-
-
5
new :name => fields.shift,
-
:mode => strict_oct(fields.shift),
-
:uid => oct_or_256based(fields.shift),
-
:gid => oct_or_256based(fields.shift),
-
:size => strict_oct(fields.shift),
-
:mtime => strict_oct(fields.shift),
-
:checksum => strict_oct(fields.shift),
-
:typeflag => fields.shift,
-
:linkname => fields.shift,
-
:magic => fields.shift,
-
:version => strict_oct(fields.shift),
-
:uname => fields.shift,
-
:gname => fields.shift,
-
:devmajor => strict_oct(fields.shift),
-
:devminor => strict_oct(fields.shift),
-
:prefix => fields.shift,
-
-
:empty => empty
-
end
-
-
1
def self.strict_oct(str)
-
45
return str.oct if str =~ /\A[0-7]*\z/
-
raise ArgumentError, "#{str.inspect} is not an octal string"
-
end
-
-
1
def self.oct_or_256based(str)
-
# \x80 flags a positive 256-based number
-
# \ff flags a negative 256-based number
-
# In case we have a match, parse it as a signed binary value
-
# in big-endian order, except that the high-order bit is ignored.
-
10
return str.unpack('N2').last if str =~ /\A[\x80\xff]/n
-
10
strict_oct(str)
-
end
-
-
##
-
# Creates a new TarHeader using +vals+
-
-
1
def initialize(vals)
-
11
unless vals[:name] && vals[:size] && vals[:prefix] && vals[:mode]
-
raise ArgumentError, ":name, :size, :prefix and :mode required"
-
end
-
-
11
vals[:uid] ||= 0
-
11
vals[:gid] ||= 0
-
11
vals[:mtime] ||= 0
-
11
vals[:checksum] ||= ""
-
11
vals[:typeflag] = "0" if vals[:typeflag].nil? || vals[:typeflag].empty?
-
11
vals[:magic] ||= "ustar"
-
11
vals[:version] ||= "00"
-
11
vals[:uname] ||= "wheel"
-
11
vals[:gname] ||= "wheel"
-
11
vals[:devmajor] ||= 0
-
11
vals[:devminor] ||= 0
-
-
11
FIELDS.each do |name|
-
176
instance_variable_set "@#{name}", vals[name]
-
end
-
-
11
@empty = vals[:empty]
-
end
-
-
##
-
# Is the tar entry empty?
-
-
1
def empty?
-
5
@empty
-
end
-
-
1
def ==(other) # :nodoc:
-
self.class === other and
-
@checksum == other.checksum and
-
@devmajor == other.devmajor and
-
@devminor == other.devminor and
-
@gid == other.gid and
-
@gname == other.gname and
-
@linkname == other.linkname and
-
@magic == other.magic and
-
@mode == other.mode and
-
@mtime == other.mtime and
-
@name == other.name and
-
@prefix == other.prefix and
-
@size == other.size and
-
@typeflag == other.typeflag and
-
@uid == other.uid and
-
@uname == other.uname and
-
@version == other.version
-
end
-
-
1
def to_s # :nodoc:
-
6
update_checksum
-
6
header
-
end
-
-
##
-
# Updates the TarHeader's checksum
-
-
1
def update_checksum
-
6
header = header " " * 8
-
6
@checksum = oct calculate_checksum(header), 6
-
end
-
-
1
private
-
-
1
def calculate_checksum(header)
-
3072
header.unpack("C*").inject { |a, b| a + b }
-
end
-
-
1
def header(checksum = @checksum)
-
header = [
-
12
name,
-
oct(mode, 7),
-
oct(uid, 7),
-
oct(gid, 7),
-
oct(size, 11),
-
oct(mtime, 11),
-
checksum,
-
" ",
-
typeflag,
-
linkname,
-
magic,
-
oct(version, 2),
-
uname,
-
gname,
-
oct(devmajor, 7),
-
oct(devminor, 7),
-
prefix
-
]
-
-
12
header = header.pack PACK_FORMAT
-
-
12
header << ("\0" * ((512 - header.size) % 512))
-
end
-
-
1
def oct(num, len)
-
102
"%0#{len}o" % num
-
end
-
-
end
-
# -*- coding: utf-8 -*-
-
# frozen_string_literal: true
-
#++
-
# Copyright (C) 2004 Mauricio Julio Fernández Pradier
-
# See LICENSE.txt for additional licensing information.
-
#--
-
-
##
-
# Class for reading entries out of a tar file
-
-
1
class Gem::Package::TarReader::Entry
-
-
##
-
# Header for this tar entry
-
-
1
attr_reader :header
-
-
##
-
# Creates a new tar entry for +header+ that will be read from +io+
-
-
1
def initialize(header, io)
-
5
@closed = false
-
5
@header = header
-
5
@io = io
-
5
@orig_pos = @io.pos
-
5
@read = 0
-
end
-
-
1
def check_closed # :nodoc:
-
5
raise IOError, "closed #{self.class}" if closed?
-
end
-
-
##
-
# Number of bytes read out of the tar entry
-
-
1
def bytes_read
-
5
@read
-
end
-
-
##
-
# Closes the tar entry
-
-
1
def close
-
5
@closed = true
-
end
-
-
##
-
# Is the tar entry closed?
-
-
1
def closed?
-
5
@closed
-
end
-
-
##
-
# Are we at the end of the tar entry?
-
-
1
def eof?
-
check_closed
-
-
@read >= @header.size
-
end
-
-
##
-
# Full name of the tar entry
-
-
1
def full_name
-
5
if @header.prefix != ""
-
File.join @header.prefix, @header.name
-
else
-
5
@header.name
-
end
-
rescue ArgumentError => e
-
raise unless e.message == 'string contains null byte'
-
raise Gem::Package::TarInvalidError,
-
'tar is corrupt, name contains null byte'
-
end
-
-
##
-
# Read one byte from the tar entry
-
-
1
def getc
-
check_closed
-
-
return nil if @read >= @header.size
-
-
ret = @io.getc
-
@read += 1 if ret
-
-
ret
-
end
-
-
##
-
# Is this tar entry a directory?
-
-
1
def directory?
-
@header.typeflag == "5"
-
end
-
-
##
-
# Is this tar entry a file?
-
-
1
def file?
-
@header.typeflag == "0"
-
end
-
-
##
-
# Is this tar entry a symlink?
-
-
1
def symlink?
-
@header.typeflag == "2"
-
end
-
-
##
-
# The position in the tar entry
-
-
1
def pos
-
check_closed
-
-
bytes_read
-
end
-
-
1
def size
-
@header.size
-
end
-
-
1
alias length size
-
-
##
-
# Reads +len+ bytes from the tar file entry, or the rest of the entry if
-
# nil
-
-
1
def read(len = nil)
-
5
check_closed
-
-
5
return nil if @read >= @header.size
-
-
4
len ||= @header.size - @read
-
4
max_read = [len, @header.size - @read].min
-
-
4
ret = @io.read max_read
-
4
@read += ret.size
-
-
4
ret
-
end
-
-
1
def readpartial(maxlen = nil, outbuf = "".b)
-
check_closed
-
-
raise EOFError if @read >= @header.size
-
-
maxlen ||= @header.size - @read
-
max_read = [maxlen, @header.size - @read].min
-
-
@io.readpartial(max_read, outbuf)
-
@read += outbuf.size
-
-
outbuf
-
end
-
-
##
-
# Rewinds to the beginning of the tar file entry
-
-
1
def rewind
-
check_closed
-
-
@io.pos = @orig_pos
-
@read = 0
-
end
-
-
end
-
# -*- coding: utf-8 -*-
-
# frozen_string_literal: true
-
#--
-
# Copyright (C) 2004 Mauricio Julio Fernández Pradier
-
# See LICENSE.txt for additional licensing information.
-
#++
-
-
1
require 'digest'
-
-
##
-
# Allows writing of tar files
-
-
1
class Gem::Package::TarWriter
-
-
1
class FileOverflow < StandardError; end
-
-
##
-
# IO wrapper that allows writing a limited amount of data
-
-
1
class BoundedStream
-
-
##
-
# Maximum number of bytes that can be written
-
-
1
attr_reader :limit
-
-
##
-
# Number of bytes written
-
-
1
attr_reader :written
-
-
##
-
# Wraps +io+ and allows up to +limit+ bytes to be written
-
-
1
def initialize(io, limit)
-
6
@io = io
-
6
@limit = limit
-
6
@written = 0
-
end
-
-
##
-
# Writes +data+ onto the IO, raising a FileOverflow exception if the
-
# number of bytes will be more than #limit
-
-
1
def write(data)
-
6
if data.bytesize + @written > @limit
-
raise FileOverflow, "You tried to feed more data than fits in the file."
-
end
-
6
@io.write data
-
6
@written += data.bytesize
-
6
data.bytesize
-
end
-
-
end
-
-
##
-
# IO wrapper that provides only #write
-
-
1
class RestrictedStream
-
-
##
-
# Creates a new RestrictedStream wrapping +io+
-
-
1
def initialize(io)
-
@io = io
-
end
-
-
##
-
# Writes +data+ onto the IO
-
-
1
def write(data)
-
@io.write data
-
end
-
-
end
-
-
##
-
# Creates a new TarWriter, yielding it if a block is given
-
-
1
def self.new(io)
-
5
writer = super
-
-
5
return writer unless block_given?
-
-
begin
-
yield writer
-
ensure
-
writer.close
-
end
-
-
nil
-
end
-
-
##
-
# Creates a new TarWriter that will write to +io+
-
-
1
def initialize(io)
-
5
@io = io
-
5
@closed = false
-
end
-
-
##
-
# Adds file +name+ with permissions +mode+, and yields an IO for writing the
-
# file to
-
-
1
def add_file(name, mode) # :yields: io
-
check_closed
-
-
name, prefix = split_name name
-
-
init_pos = @io.pos
-
@io.write Gem::Package::TarHeader::EMPTY_HEADER # placeholder for the header
-
-
yield RestrictedStream.new(@io) if block_given?
-
-
size = @io.pos - init_pos - 512
-
-
remainder = (512 - (size % 512)) % 512
-
@io.write "\0" * remainder
-
-
final_pos = @io.pos
-
@io.pos = init_pos
-
-
header = Gem::Package::TarHeader.new :name => name, :mode => mode,
-
:size => size, :prefix => prefix,
-
:mtime => Gem.source_date_epoch
-
-
@io.write header
-
@io.pos = final_pos
-
-
self
-
end
-
-
##
-
# Adds +name+ with permissions +mode+ to the tar, yielding +io+ for writing
-
# the file. The +digest_algorithm+ is written to a read-only +name+.sum
-
# file following the given file contents containing the digest name and
-
# hexdigest separated by a tab.
-
#
-
# The created digest object is returned.
-
-
1
def add_file_digest(name, mode, digest_algorithms) # :yields: io
-
digests = digest_algorithms.map do |digest_algorithm|
-
digest = digest_algorithm.new
-
digest_name =
-
if digest.respond_to? :name
-
digest.name
-
else
-
/::([^:]+)$/ =~ digest_algorithm.name
-
$1
-
end
-
-
[digest_name, digest]
-
end
-
-
digests = Hash[*digests.flatten]
-
-
add_file name, mode do |io|
-
Gem::Package::DigestIO.wrap io, digests do |digest_io|
-
yield digest_io
-
end
-
end
-
-
digests
-
end
-
-
##
-
# Adds +name+ with permissions +mode+ to the tar, yielding +io+ for writing
-
# the file. The +signer+ is used to add a digest file using its
-
# digest_algorithm per add_file_digest and a cryptographic signature in
-
# +name+.sig. If the signer has no key only the checksum file is added.
-
#
-
# Returns the digest.
-
-
1
def add_file_signed(name, mode, signer)
-
digest_algorithms = [
-
signer.digest_algorithm,
-
Digest::SHA512,
-
].compact.uniq
-
-
digests = add_file_digest name, mode, digest_algorithms do |io|
-
yield io
-
end
-
-
signature_digest = digests.values.compact.find do |digest|
-
digest_name =
-
if digest.respond_to? :name
-
digest.name
-
else
-
digest.class.name[/::([^:]+)\z/, 1]
-
end
-
-
digest_name == signer.digest_name
-
end
-
-
raise "no #{signer.digest_name} in #{digests.values.compact}" unless signature_digest
-
-
if signer.key
-
signature = signer.sign signature_digest.digest
-
-
add_file_simple "#{name}.sig", 0444, signature.length do |io|
-
io.write signature
-
end
-
end
-
-
digests
-
end
-
-
##
-
# Add file +name+ with permissions +mode+ +size+ bytes long. Yields an IO
-
# to write the file to.
-
-
1
def add_file_simple(name, mode, size) # :yields: io
-
6
check_closed
-
-
6
name, prefix = split_name name
-
-
6
header = Gem::Package::TarHeader.new(:name => name, :mode => mode,
-
:size => size, :prefix => prefix,
-
:mtime => Gem.source_date_epoch).to_s
-
-
6
@io.write header
-
6
os = BoundedStream.new @io, size
-
-
6
yield os if block_given?
-
-
6
min_padding = size - os.written
-
6
@io.write("\0" * min_padding)
-
-
6
remainder = (512 - (size % 512)) % 512
-
6
@io.write("\0" * remainder)
-
-
6
self
-
end
-
-
##
-
# Adds symlink +name+ with permissions +mode+, linking to +target+.
-
-
1
def add_symlink(name, target, mode)
-
check_closed
-
-
name, prefix = split_name name
-
-
header = Gem::Package::TarHeader.new(:name => name, :mode => mode,
-
:size => 0, :typeflag => "2",
-
:linkname => target,
-
:prefix => prefix,
-
:mtime => Gem.source_date_epoch).to_s
-
-
@io.write header
-
-
self
-
end
-
-
##
-
# Raises IOError if the TarWriter is closed
-
-
1
def check_closed
-
6
raise IOError, "closed #{self.class}" if closed?
-
end
-
-
##
-
# Closes the TarWriter
-
-
1
def close
-
check_closed
-
-
@io.write "\0" * 1024
-
flush
-
-
@closed = true
-
end
-
-
##
-
# Is the TarWriter closed?
-
-
1
def closed?
-
6
@closed
-
end
-
-
##
-
# Flushes the TarWriter's IO
-
-
1
def flush
-
check_closed
-
-
@io.flush if @io.respond_to? :flush
-
end
-
-
##
-
# Creates a new directory in the tar file +name+ with +mode+
-
-
1
def mkdir(name, mode)
-
check_closed
-
-
name, prefix = split_name(name)
-
-
header = Gem::Package::TarHeader.new :name => name, :mode => mode,
-
:typeflag => "5", :size => 0,
-
:prefix => prefix,
-
:mtime => Gem.source_date_epoch
-
-
@io.write header
-
-
self
-
end
-
-
##
-
# Splits +name+ into a name and prefix that can fit in the TarHeader
-
-
1
def split_name(name) # :nodoc:
-
6
if name.bytesize > 256
-
raise Gem::Package::TooLongFileName.new("File \"#{name}\" has a too long path (should be 256 or less)")
-
end
-
-
6
prefix = ''
-
6
if name.bytesize > 100
-
parts = name.split('/', -1) # parts are never empty here
-
name = parts.pop # initially empty for names with a trailing slash ("foo/.../bar/")
-
prefix = parts.join('/') # if empty, then it's impossible to split (parts is empty too)
-
while !parts.empty? && (prefix.bytesize > 155 || name.empty?)
-
name = parts.pop + '/' + name
-
prefix = parts.join('/')
-
end
-
-
if name.bytesize > 100 or prefix.empty?
-
raise Gem::Package::TooLongFileName.new("File \"#{prefix}/#{name}\" has a too long name (should be 100 or less)")
-
end
-
-
if prefix.bytesize > 155
-
raise Gem::Package::TooLongFileName.new("File \"#{prefix}/#{name}\" has a too long base path (should be 155 or less)")
-
end
-
end
-
-
6
return name, prefix
-
end
-
-
end
-
# frozen_string_literal: true
-
#--
-
# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others.
-
# All rights reserved.
-
# See LICENSE.txt for permissions.
-
#++
-
-
1
require 'rubygems/exceptions'
-
1
require 'fileutils'
-
-
begin
-
1
require 'openssl'
-
rescue LoadError => e
-
raise unless (e.respond_to?(:path) && e.path == 'openssl') ||
-
e.message =~ / -- openssl$/
-
end
-
-
##
-
# = Signing gems
-
#
-
# The Gem::Security implements cryptographic signatures for gems. The section
-
# below is a step-by-step guide to using signed gems and generating your own.
-
#
-
# == Walkthrough
-
#
-
# === Building your certificate
-
#
-
# In order to start signing your gems, you'll need to build a private key and
-
# a self-signed certificate. Here's how:
-
#
-
# # build a private key and certificate for yourself:
-
# $ gem cert --build you@example.com
-
#
-
# This could take anywhere from a few seconds to a minute or two, depending on
-
# the speed of your computer (public key algorithms aren't exactly the
-
# speediest crypto algorithms in the world). When it's finished, you'll see
-
# the files "gem-private_key.pem" and "gem-public_cert.pem" in the current
-
# directory.
-
#
-
# First things first: Move both files to ~/.gem if you don't already have a
-
# key and certificate in that directory. Ensure the file permissions make the
-
# key unreadable by others (by default the file is saved securely).
-
#
-
# Keep your private key hidden; if it's compromised, someone can sign packages
-
# as you (note: PKI has ways of mitigating the risk of stolen keys; more on
-
# that later).
-
#
-
# === Signing Gems
-
#
-
# In RubyGems 2 and newer there is no extra work to sign a gem. RubyGems will
-
# automatically find your key and certificate in your home directory and use
-
# them to sign newly packaged gems.
-
#
-
# If your certificate is not self-signed (signed by a third party) RubyGems
-
# will attempt to load the certificate chain from the trusted certificates.
-
# Use <code>gem cert --add signing_cert.pem</code> to add your signers as
-
# trusted certificates. See below for further information on certificate
-
# chains.
-
#
-
# If you build your gem it will automatically be signed. If you peek inside
-
# your gem file, you'll see a couple of new files have been added:
-
#
-
# $ tar tf your-gem-1.0.gem
-
# metadata.gz
-
# metadata.gz.sum
-
# metadata.gz.sig # metadata signature
-
# data.tar.gz
-
# data.tar.gz.sum
-
# data.tar.gz.sig # data signature
-
#
-
# === Manually signing gems
-
#
-
# If you wish to store your key in a separate secure location you'll need to
-
# set your gems up for signing by hand. To do this, set the
-
# <code>signing_key</code> and <code>cert_chain</code> in the gemspec before
-
# packaging your gem:
-
#
-
# s.signing_key = '/secure/path/to/gem-private_key.pem'
-
# s.cert_chain = %w[/secure/path/to/gem-public_cert.pem]
-
#
-
# When you package your gem with these options set RubyGems will automatically
-
# load your key and certificate from the secure paths.
-
#
-
# === Signed gems and security policies
-
#
-
# Now let's verify the signature. Go ahead and install the gem, but add the
-
# following options: <code>-P HighSecurity</code>, like this:
-
#
-
# # install the gem with using the security policy "HighSecurity"
-
# $ sudo gem install your.gem -P HighSecurity
-
#
-
# The <code>-P</code> option sets your security policy -- we'll talk about
-
# that in just a minute. Eh, what's this?
-
#
-
# $ gem install -P HighSecurity your-gem-1.0.gem
-
# ERROR: While executing gem ... (Gem::Security::Exception)
-
# root cert /CN=you/DC=example is not trusted
-
#
-
# The culprit here is the security policy. RubyGems has several different
-
# security policies. Let's take a short break and go over the security
-
# policies. Here's a list of the available security policies, and a brief
-
# description of each one:
-
#
-
# * NoSecurity - Well, no security at all. Signed packages are treated like
-
# unsigned packages.
-
# * LowSecurity - Pretty much no security. If a package is signed then
-
# RubyGems will make sure the signature matches the signing
-
# certificate, and that the signing certificate hasn't expired, but
-
# that's it. A malicious user could easily circumvent this kind of
-
# security.
-
# * MediumSecurity - Better than LowSecurity and NoSecurity, but still
-
# fallible. Package contents are verified against the signing
-
# certificate, and the signing certificate is checked for validity,
-
# and checked against the rest of the certificate chain (if you don't
-
# know what a certificate chain is, stay tuned, we'll get to that).
-
# The biggest improvement over LowSecurity is that MediumSecurity
-
# won't install packages that are signed by untrusted sources.
-
# Unfortunately, MediumSecurity still isn't totally secure -- a
-
# malicious user can still unpack the gem, strip the signatures, and
-
# distribute the gem unsigned.
-
# * HighSecurity - Here's the bugger that got us into this mess.
-
# The HighSecurity policy is identical to the MediumSecurity policy,
-
# except that it does not allow unsigned gems. A malicious user
-
# doesn't have a whole lot of options here; they can't modify the
-
# package contents without invalidating the signature, and they can't
-
# modify or remove signature or the signing certificate chain, or
-
# RubyGems will simply refuse to install the package. Oh well, maybe
-
# they'll have better luck causing problems for CPAN users instead :).
-
#
-
# The reason RubyGems refused to install your shiny new signed gem was because
-
# it was from an untrusted source. Well, your code is infallible (naturally),
-
# so you need to add yourself as a trusted source:
-
#
-
# # add trusted certificate
-
# gem cert --add ~/.gem/gem-public_cert.pem
-
#
-
# You've now added your public certificate as a trusted source. Now you can
-
# install packages signed by your private key without any hassle. Let's try
-
# the install command above again:
-
#
-
# # install the gem with using the HighSecurity policy (and this time
-
# # without any shenanigans)
-
# $ gem install -P HighSecurity your-gem-1.0.gem
-
# Successfully installed your-gem-1.0
-
# 1 gem installed
-
#
-
# This time RubyGems will accept your signed package and begin installing.
-
#
-
# While you're waiting for RubyGems to work it's magic, have a look at some of
-
# the other security commands by running <code>gem help cert</code>:
-
#
-
# Options:
-
# -a, --add CERT Add a trusted certificate.
-
# -l, --list [FILTER] List trusted certificates where the
-
# subject contains FILTER
-
# -r, --remove FILTER Remove trusted certificates where the
-
# subject contains FILTER
-
# -b, --build EMAIL_ADDR Build private key and self-signed
-
# certificate for EMAIL_ADDR
-
# -C, --certificate CERT Signing certificate for --sign
-
# -K, --private-key KEY Key for --sign or --build
-
# -s, --sign CERT Signs CERT with the key from -K
-
# and the certificate from -C
-
#
-
# We've already covered the <code>--build</code> option, and the
-
# <code>--add</code>, <code>--list</code>, and <code>--remove</code> commands
-
# seem fairly straightforward; they allow you to add, list, and remove the
-
# certificates in your trusted certificate list. But what's with this
-
# <code>--sign</code> option?
-
#
-
# === Certificate chains
-
#
-
# To answer that question, let's take a look at "certificate chains", a
-
# concept I mentioned earlier. There are a couple of problems with
-
# self-signed certificates: first of all, self-signed certificates don't offer
-
# a whole lot of security. Sure, the certificate says Yukihiro Matsumoto, but
-
# how do I know it was actually generated and signed by matz himself unless he
-
# gave me the certificate in person?
-
#
-
# The second problem is scalability. Sure, if there are 50 gem authors, then
-
# I have 50 trusted certificates, no problem. What if there are 500 gem
-
# authors? 1000? Having to constantly add new trusted certificates is a
-
# pain, and it actually makes the trust system less secure by encouraging
-
# RubyGems users to blindly trust new certificates.
-
#
-
# Here's where certificate chains come in. A certificate chain establishes an
-
# arbitrarily long chain of trust between an issuing certificate and a child
-
# certificate. So instead of trusting certificates on a per-developer basis,
-
# we use the PKI concept of certificate chains to build a logical hierarchy of
-
# trust. Here's a hypothetical example of a trust hierarchy based (roughly)
-
# on geography:
-
#
-
# --------------------------
-
# | rubygems@rubygems.org |
-
# --------------------------
-
# |
-
# -----------------------------------
-
# | |
-
# ---------------------------- -----------------------------
-
# | seattlerb@seattlerb.org | | dcrubyists@richkilmer.com |
-
# ---------------------------- -----------------------------
-
# | | | |
-
# --------------- ---------------- ----------- --------------
-
# | drbrain | | zenspider | | pabs@dc | | tomcope@dc |
-
# --------------- ---------------- ----------- --------------
-
#
-
#
-
# Now, rather than having 4 trusted certificates (one for drbrain, zenspider,
-
# pabs@dc, and tomecope@dc), a user could actually get by with one
-
# certificate, the "rubygems@rubygems.org" certificate.
-
#
-
# Here's how it works:
-
#
-
# I install "rdoc-3.12.gem", a package signed by "drbrain". I've never heard
-
# of "drbrain", but his certificate has a valid signature from the
-
# "seattle.rb@seattlerb.org" certificate, which in turn has a valid signature
-
# from the "rubygems@rubygems.org" certificate. Voila! At this point, it's
-
# much more reasonable for me to trust a package signed by "drbrain", because
-
# I can establish a chain to "rubygems@rubygems.org", which I do trust.
-
#
-
# === Signing certificates
-
#
-
# The <code>--sign</code> option allows all this to happen. A developer
-
# creates their build certificate with the <code>--build</code> option, then
-
# has their certificate signed by taking it with them to their next regional
-
# Ruby meetup (in our hypothetical example), and it's signed there by the
-
# person holding the regional RubyGems signing certificate, which is signed at
-
# the next RubyConf by the holder of the top-level RubyGems certificate. At
-
# each point the issuer runs the same command:
-
#
-
# # sign a certificate with the specified key and certificate
-
# # (note that this modifies client_cert.pem!)
-
# $ gem cert -K /mnt/floppy/issuer-priv_key.pem -C issuer-pub_cert.pem
-
# --sign client_cert.pem
-
#
-
# Then the holder of issued certificate (in this case, your buddy "drbrain"),
-
# can start using this signed certificate to sign RubyGems. By the way, in
-
# order to let everyone else know about his new fancy signed certificate,
-
# "drbrain" would save his newly signed certificate as
-
# <code>~/.gem/gem-public_cert.pem</code>
-
#
-
# Obviously this RubyGems trust infrastructure doesn't exist yet. Also, in
-
# the "real world", issuers actually generate the child certificate from a
-
# certificate request, rather than sign an existing certificate. And our
-
# hypothetical infrastructure is missing a certificate revocation system.
-
# These are that can be fixed in the future...
-
#
-
# At this point you should know how to do all of these new and interesting
-
# things:
-
#
-
# * build a gem signing key and certificate
-
# * adjust your security policy
-
# * modify your trusted certificate list
-
# * sign a certificate
-
#
-
# == Manually verifying signatures
-
#
-
# In case you don't trust RubyGems you can verify gem signatures manually:
-
#
-
# 1. Fetch and unpack the gem
-
#
-
# gem fetch some_signed_gem
-
# tar -xf some_signed_gem-1.0.gem
-
#
-
# 2. Grab the public key from the gemspec
-
#
-
# gem spec some_signed_gem-1.0.gem cert_chain | \
-
# ruby -ryaml -e 'puts YAML.load_documents($stdin)' > public_key.crt
-
#
-
# 3. Generate a SHA1 hash of the data.tar.gz
-
#
-
# openssl dgst -sha1 < data.tar.gz > my.hash
-
#
-
# 4. Verify the signature
-
#
-
# openssl rsautl -verify -inkey public_key.crt -certin \
-
# -in data.tar.gz.sig > verified.hash
-
#
-
# 5. Compare your hash to the verified hash
-
#
-
# diff -s verified.hash my.hash
-
#
-
# 6. Repeat 5 and 6 with metadata.gz
-
#
-
# == OpenSSL Reference
-
#
-
# The .pem files generated by --build and --sign are PEM files. Here's a
-
# couple of useful OpenSSL commands for manipulating them:
-
#
-
# # convert a PEM format X509 certificate into DER format:
-
# # (note: Windows .cer files are X509 certificates in DER format)
-
# $ openssl x509 -in input.pem -outform der -out output.der
-
#
-
# # print out the certificate in a human-readable format:
-
# $ openssl x509 -in input.pem -noout -text
-
#
-
# And you can do the same thing with the private key file as well:
-
#
-
# # convert a PEM format RSA key into DER format:
-
# $ openssl rsa -in input_key.pem -outform der -out output_key.der
-
#
-
# # print out the key in a human readable format:
-
# $ openssl rsa -in input_key.pem -noout -text
-
#
-
# == Bugs/TODO
-
#
-
# * There's no way to define a system-wide trust list.
-
# * custom security policies (from a YAML file, etc)
-
# * Simple method to generate a signed certificate request
-
# * Support for OCSP, SCVP, CRLs, or some other form of cert status check
-
# (list is in order of preference)
-
# * Support for encrypted private keys
-
# * Some sort of semi-formal trust hierarchy (see long-winded explanation
-
# above)
-
# * Path discovery (for gem certificate chains that don't have a self-signed
-
# root) -- by the way, since we don't have this, THE ROOT OF THE CERTIFICATE
-
# CHAIN MUST BE SELF SIGNED if Policy#verify_root is true (and it is for the
-
# MediumSecurity and HighSecurity policies)
-
# * Better explanation of X509 naming (ie, we don't have to use email
-
# addresses)
-
# * Honor AIA field (see note about OCSP above)
-
# * Honor extension restrictions
-
# * Might be better to store the certificate chain as a PKCS#7 or PKCS#12
-
# file, instead of an array embedded in the metadata.
-
# * Flexible signature and key algorithms, not hard-coded to RSA and SHA1.
-
#
-
# == Original author
-
#
-
# Paul Duncan <pabs@pablotron.org>
-
# http://pablotron.org/
-
-
1
module Gem::Security
-
-
##
-
# Gem::Security default exception type
-
-
1
class Exception < Gem::Exception; end
-
-
##
-
# Digest algorithm used to sign gems
-
-
DIGEST_ALGORITHM =
-
1
if defined?(OpenSSL::Digest::SHA256)
-
1
OpenSSL::Digest::SHA256
-
elsif defined?(OpenSSL::Digest::SHA1)
-
OpenSSL::Digest::SHA1
-
else
-
require 'digest'
-
Digest::SHA512
-
end
-
-
##
-
# Used internally to select the signing digest from all computed digests
-
-
DIGEST_NAME = # :nodoc:
-
1
if DIGEST_ALGORITHM.method_defined? :name
-
1
DIGEST_ALGORITHM.new.name
-
else
-
DIGEST_ALGORITHM.name[/::([^:]+)\z/, 1]
-
end
-
-
##
-
# Algorithm for creating the key pair used to sign gems
-
-
KEY_ALGORITHM =
-
1
if defined?(OpenSSL::PKey::RSA)
-
1
OpenSSL::PKey::RSA
-
end
-
-
##
-
# Length of keys created by KEY_ALGORITHM
-
-
1
KEY_LENGTH = 3072
-
-
##
-
# Cipher used to encrypt the key pair used to sign gems.
-
# Must be in the list returned by OpenSSL::Cipher.ciphers
-
-
1
KEY_CIPHER = OpenSSL::Cipher.new('AES-256-CBC') if defined?(OpenSSL::Cipher)
-
-
##
-
# One day in seconds
-
-
1
ONE_DAY = 86400
-
-
##
-
# One year in seconds
-
-
1
ONE_YEAR = ONE_DAY * 365
-
-
##
-
# The default set of extensions are:
-
#
-
# * The certificate is not a certificate authority
-
# * The key for the certificate may be used for key and data encipherment
-
# and digital signatures
-
# * The certificate contains a subject key identifier
-
-
1
EXTENSIONS = {
-
'basicConstraints' => 'CA:FALSE',
-
'keyUsage' =>
-
'keyEncipherment,dataEncipherment,digitalSignature',
-
'subjectKeyIdentifier' => 'hash',
-
}.freeze
-
-
1
def self.alt_name_or_x509_entry(certificate, x509_entry)
-
alt_name = certificate.extensions.find do |extension|
-
extension.oid == "#{x509_entry}AltName"
-
end
-
-
return alt_name.value if alt_name
-
-
certificate.send x509_entry
-
end
-
-
##
-
# Creates an unsigned certificate for +subject+ and +key+. The lifetime of
-
# the key is from the current time to +age+ which defaults to one year.
-
#
-
# The +extensions+ restrict the key to the indicated uses.
-
-
1
def self.create_cert(subject, key, age = ONE_YEAR, extensions = EXTENSIONS,
-
serial = 1)
-
cert = OpenSSL::X509::Certificate.new
-
-
cert.public_key = key.public_key
-
cert.version = 2
-
cert.serial = serial
-
-
cert.not_before = Time.now
-
cert.not_after = Time.now + age
-
-
cert.subject = subject
-
-
ef = OpenSSL::X509::ExtensionFactory.new nil, cert
-
-
cert.extensions = extensions.map do |ext_name, value|
-
ef.create_extension ext_name, value
-
end
-
-
cert
-
end
-
-
##
-
# Creates a self-signed certificate with an issuer and subject from +email+,
-
# a subject alternative name of +email+ and the given +extensions+ for the
-
# +key+.
-
-
1
def self.create_cert_email(email, key, age = ONE_YEAR, extensions = EXTENSIONS)
-
subject = email_to_name email
-
-
extensions = extensions.merge "subjectAltName" => "email:#{email}"
-
-
create_cert_self_signed subject, key, age, extensions
-
end
-
-
##
-
# Creates a self-signed certificate with an issuer and subject of +subject+
-
# and the given +extensions+ for the +key+.
-
-
1
def self.create_cert_self_signed(subject, key, age = ONE_YEAR,
-
extensions = EXTENSIONS, serial = 1)
-
certificate = create_cert subject, key, age, extensions
-
-
sign certificate, key, certificate, age, extensions, serial
-
end
-
-
##
-
# Creates a new key pair of the specified +length+ and +algorithm+. The
-
# default is a 3072 bit RSA key.
-
-
1
def self.create_key(length = KEY_LENGTH, algorithm = KEY_ALGORITHM)
-
algorithm.new length
-
end
-
-
##
-
# Turns +email_address+ into an OpenSSL::X509::Name
-
-
1
def self.email_to_name(email_address)
-
email_address = email_address.gsub(/[^\w@.-]+/i, '_')
-
-
cn, dcs = email_address.split '@'
-
-
dcs = dcs.split '.'
-
-
name = "CN=#{cn}/#{dcs.map { |dc| "DC=#{dc}" }.join '/'}"
-
-
OpenSSL::X509::Name.parse name
-
end
-
-
##
-
# Signs +expired_certificate+ with +private_key+ if the keys match and the
-
# expired certificate was self-signed.
-
#--
-
# TODO increment serial
-
-
1
def self.re_sign(expired_certificate, private_key, age = ONE_YEAR,
-
extensions = EXTENSIONS)
-
raise Gem::Security::Exception,
-
"incorrect signing key for re-signing " +
-
"#{expired_certificate.subject}" unless
-
expired_certificate.public_key.to_pem == private_key.public_key.to_pem
-
-
unless expired_certificate.subject.to_s ==
-
expired_certificate.issuer.to_s
-
subject = alt_name_or_x509_entry expired_certificate, :subject
-
issuer = alt_name_or_x509_entry expired_certificate, :issuer
-
-
raise Gem::Security::Exception,
-
"#{subject} is not self-signed, contact #{issuer} " +
-
"to obtain a valid certificate"
-
end
-
-
serial = expired_certificate.serial + 1
-
-
create_cert_self_signed(expired_certificate.subject, private_key, age,
-
extensions, serial)
-
end
-
-
##
-
# Resets the trust directory for verifying gems.
-
-
1
def self.reset
-
1
@trust_dir = nil
-
end
-
-
##
-
# Sign the public key from +certificate+ with the +signing_key+ and
-
# +signing_cert+, using the Gem::Security::DIGEST_ALGORITHM. Uses the
-
# default certificate validity range and extensions.
-
#
-
# Returns the newly signed certificate.
-
-
1
def self.sign(certificate, signing_key, signing_cert,
-
age = ONE_YEAR, extensions = EXTENSIONS, serial = 1)
-
signee_subject = certificate.subject
-
signee_key = certificate.public_key
-
-
alt_name = certificate.extensions.find do |extension|
-
extension.oid == 'subjectAltName'
-
end
-
-
extensions = extensions.merge 'subjectAltName' => alt_name.value if
-
alt_name
-
-
issuer_alt_name = signing_cert.extensions.find do |extension|
-
extension.oid == 'subjectAltName'
-
end
-
-
extensions = extensions.merge 'issuerAltName' => issuer_alt_name.value if
-
issuer_alt_name
-
-
signed = create_cert signee_subject, signee_key, age, extensions, serial
-
signed.issuer = signing_cert.subject
-
-
signed.sign signing_key, Gem::Security::DIGEST_ALGORITHM.new
-
end
-
-
##
-
# Returns a Gem::Security::TrustDir which wraps the directory where trusted
-
# certificates live.
-
-
1
def self.trust_dir
-
return @trust_dir if @trust_dir
-
-
dir = File.join Gem.user_home, '.gem', 'trust'
-
-
@trust_dir ||= Gem::Security::TrustDir.new dir
-
end
-
-
##
-
# Enumerates the trusted certificates via Gem::Security::TrustDir.
-
-
1
def self.trusted_certificates(&block)
-
trust_dir.each_certificate(&block)
-
end
-
-
##
-
# Writes +pemmable+, which must respond to +to_pem+ to +path+ with the given
-
# +permissions+. If passed +cipher+ and +passphrase+ those arguments will be
-
# passed to +to_pem+.
-
-
1
def self.write(pemmable, path, permissions = 0600, passphrase = nil, cipher = KEY_CIPHER)
-
path = File.expand_path path
-
-
File.open path, 'wb', permissions do |io|
-
if passphrase and cipher
-
io.write pemmable.to_pem cipher, passphrase
-
else
-
io.write pemmable.to_pem
-
end
-
end
-
-
path
-
end
-
-
1
reset
-
-
end
-
-
1
if defined?(OpenSSL::SSL)
-
1
require 'rubygems/security/policy'
-
1
require 'rubygems/security/policies'
-
1
require 'rubygems/security/trust_dir'
-
end
-
-
1
require 'rubygems/security/signer'
-
# frozen_string_literal: true
-
1
module Gem::Security
-
-
##
-
# No security policy: all package signature checks are disabled.
-
-
1
NoSecurity = Policy.new(
-
'No Security',
-
:verify_data => false,
-
:verify_signer => false,
-
:verify_chain => false,
-
:verify_root => false,
-
:only_trusted => false,
-
:only_signed => false
-
)
-
-
##
-
# AlmostNo security policy: only verify that the signing certificate is the
-
# one that actually signed the data. Make no attempt to verify the signing
-
# certificate chain.
-
#
-
# This policy is basically useless. better than nothing, but can still be
-
# easily spoofed, and is not recommended.
-
-
1
AlmostNoSecurity = Policy.new(
-
'Almost No Security',
-
:verify_data => true,
-
:verify_signer => false,
-
:verify_chain => false,
-
:verify_root => false,
-
:only_trusted => false,
-
:only_signed => false
-
)
-
-
##
-
# Low security policy: only verify that the signing certificate is actually
-
# the gem signer, and that the signing certificate is valid.
-
#
-
# This policy is better than nothing, but can still be easily spoofed, and
-
# is not recommended.
-
-
1
LowSecurity = Policy.new(
-
'Low Security',
-
:verify_data => true,
-
:verify_signer => true,
-
:verify_chain => false,
-
:verify_root => false,
-
:only_trusted => false,
-
:only_signed => false
-
)
-
-
##
-
# Medium security policy: verify the signing certificate, verify the signing
-
# certificate chain all the way to the root certificate, and only trust root
-
# certificates that we have explicitly allowed trust for.
-
#
-
# This security policy is reasonable, but it allows unsigned packages, so a
-
# malicious person could simply delete the package signature and pass the
-
# gem off as unsigned.
-
-
1
MediumSecurity = Policy.new(
-
'Medium Security',
-
:verify_data => true,
-
:verify_signer => true,
-
:verify_chain => true,
-
:verify_root => true,
-
:only_trusted => true,
-
:only_signed => false
-
)
-
-
##
-
# High security policy: only allow signed gems to be installed, verify the
-
# signing certificate, verify the signing certificate chain all the way to
-
# the root certificate, and only trust root certificates that we have
-
# explicitly allowed trust for.
-
#
-
# This security policy is significantly more difficult to bypass, and offers
-
# a reasonable guarantee that the contents of the gem have not been altered.
-
-
1
HighSecurity = Policy.new(
-
'High Security',
-
:verify_data => true,
-
:verify_signer => true,
-
:verify_chain => true,
-
:verify_root => true,
-
:only_trusted => true,
-
:only_signed => true
-
)
-
-
##
-
# Policy used to verify a certificate and key when signing a gem
-
-
1
SigningPolicy = Policy.new(
-
'Signing Policy',
-
:verify_data => false,
-
:verify_signer => true,
-
:verify_chain => true,
-
:verify_root => true,
-
:only_trusted => false,
-
:only_signed => false
-
)
-
-
##
-
# Hash of configured security policies
-
-
Policies = {
-
1
'NoSecurity' => NoSecurity,
-
'AlmostNoSecurity' => AlmostNoSecurity,
-
'LowSecurity' => LowSecurity,
-
'MediumSecurity' => MediumSecurity,
-
'HighSecurity' => HighSecurity,
-
# SigningPolicy is not intended for use by `gem -P` so do not list it
-
}.freeze
-
-
end
-
# frozen_string_literal: true
-
1
require 'rubygems/user_interaction'
-
-
##
-
# A Gem::Security::Policy object encapsulates the settings for verifying
-
# signed gem files. This is the base class. You can either declare an
-
# instance of this or use one of the preset security policies in
-
# Gem::Security::Policies.
-
-
1
class Gem::Security::Policy
-
-
1
include Gem::UserInteraction
-
-
1
attr_reader :name
-
-
1
attr_accessor :only_signed
-
1
attr_accessor :only_trusted
-
1
attr_accessor :verify_chain
-
1
attr_accessor :verify_data
-
1
attr_accessor :verify_root
-
1
attr_accessor :verify_signer
-
-
##
-
# Create a new Gem::Security::Policy object with the given mode and
-
# options.
-
-
1
def initialize(name, policy = {}, opt = {})
-
6
require 'openssl'
-
-
6
@name = name
-
-
6
@opt = opt
-
-
# Default to security
-
6
@only_signed = true
-
6
@only_trusted = true
-
6
@verify_chain = true
-
6
@verify_data = true
-
6
@verify_root = true
-
6
@verify_signer = true
-
-
6
policy.each_pair do |key, val|
-
36
case key
-
6
when :verify_data then @verify_data = val
-
6
when :verify_signer then @verify_signer = val
-
6
when :verify_chain then @verify_chain = val
-
6
when :verify_root then @verify_root = val
-
6
when :only_trusted then @only_trusted = val
-
6
when :only_signed then @only_signed = val
-
end
-
end
-
end
-
-
##
-
# Verifies each certificate in +chain+ has signed the following certificate
-
# and is valid for the given +time+.
-
-
1
def check_chain(chain, time)
-
raise Gem::Security::Exception, 'missing signing chain' unless chain
-
raise Gem::Security::Exception, 'empty signing chain' if chain.empty?
-
-
begin
-
chain.each_cons 2 do |issuer, cert|
-
check_cert cert, issuer, time
-
end
-
-
true
-
rescue Gem::Security::Exception => e
-
raise Gem::Security::Exception, "invalid signing chain: #{e.message}"
-
end
-
end
-
-
##
-
# Verifies that +data+ matches the +signature+ created by +public_key+ and
-
# the +digest+ algorithm.
-
-
1
def check_data(public_key, digest, signature, data)
-
raise Gem::Security::Exception, "invalid signature" unless
-
public_key.verify digest.new, signature, data.digest
-
-
true
-
end
-
-
##
-
# Ensures that +signer+ is valid for +time+ and was signed by the +issuer+.
-
# If the +issuer+ is +nil+ no verification is performed.
-
-
1
def check_cert(signer, issuer, time)
-
raise Gem::Security::Exception, 'missing signing certificate' unless
-
signer
-
-
message = "certificate #{signer.subject}"
-
-
if not_before = signer.not_before and not_before > time
-
raise Gem::Security::Exception,
-
"#{message} not valid before #{not_before}"
-
end
-
-
if not_after = signer.not_after and not_after < time
-
raise Gem::Security::Exception, "#{message} not valid after #{not_after}"
-
end
-
-
if issuer and not signer.verify issuer.public_key
-
raise Gem::Security::Exception,
-
"#{message} was not issued by #{issuer.subject}"
-
end
-
-
true
-
end
-
-
##
-
# Ensures the public key of +key+ matches the public key in +signer+
-
-
1
def check_key(signer, key)
-
unless signer and key
-
return true unless @only_signed
-
-
raise Gem::Security::Exception, 'missing key or signature'
-
end
-
-
raise Gem::Security::Exception,
-
"certificate #{signer.subject} does not match the signing key" unless
-
signer.public_key.to_pem == key.public_key.to_pem
-
-
true
-
end
-
-
##
-
# Ensures the root certificate in +chain+ is self-signed and valid for
-
# +time+.
-
-
1
def check_root(chain, time)
-
raise Gem::Security::Exception, 'missing signing chain' unless chain
-
-
root = chain.first
-
-
raise Gem::Security::Exception, 'missing root certificate' unless root
-
-
raise Gem::Security::Exception,
-
"root certificate #{root.subject} is not self-signed " +
-
"(issuer #{root.issuer})" if
-
root.issuer.to_s != root.subject.to_s # HACK to_s is for ruby 1.8
-
-
check_cert root, root, time
-
end
-
-
##
-
# Ensures the root of +chain+ has a trusted certificate in +trust_dir+ and
-
# the digests of the two certificates match according to +digester+
-
-
1
def check_trust(chain, digester, trust_dir)
-
raise Gem::Security::Exception, 'missing signing chain' unless chain
-
-
root = chain.first
-
-
raise Gem::Security::Exception, 'missing root certificate' unless root
-
-
path = Gem::Security.trust_dir.cert_path root
-
-
unless File.exist? path
-
message = "root cert #{root.subject} is not trusted".dup
-
-
message << " (root of signing cert #{chain.last.subject})" if
-
chain.length > 1
-
-
raise Gem::Security::Exception, message
-
end
-
-
save_cert = OpenSSL::X509::Certificate.new File.read path
-
save_dgst = digester.digest save_cert.public_key.to_s
-
-
pkey_str = root.public_key.to_s
-
cert_dgst = digester.digest pkey_str
-
-
raise Gem::Security::Exception,
-
"trusted root certificate #{root.subject} checksum " +
-
"does not match signing root certificate checksum" unless
-
save_dgst == cert_dgst
-
-
true
-
end
-
-
##
-
# Extracts the email or subject from +certificate+
-
-
1
def subject(certificate) # :nodoc:
-
certificate.extensions.each do |extension|
-
next unless extension.oid == 'subjectAltName'
-
-
return extension.value
-
end
-
-
certificate.subject.to_s
-
end
-
-
1
def inspect # :nodoc:
-
("[Policy: %s - data: %p signer: %p chain: %p root: %p " +
-
"signed-only: %p trusted-only: %p]") % [
-
@name, @verify_chain, @verify_data, @verify_root, @verify_signer,
-
@only_signed, @only_trusted,
-
]
-
end
-
-
##
-
# For +full_name+, verifies the certificate +chain+ is valid, the +digests+
-
# match the signatures +signatures+ created by the signer depending on the
-
# +policy+ settings.
-
#
-
# If +key+ is given it is used to validate the signing certificate.
-
-
1
def verify(chain, key = nil, digests = {}, signatures = {},
-
full_name = '(unknown)')
-
if signatures.empty?
-
if @only_signed
-
raise Gem::Security::Exception,
-
"unsigned gems are not allowed by the #{name} policy"
-
elsif digests.empty?
-
# lack of signatures is irrelevant if there is nothing to check
-
# against
-
else
-
alert_warning "#{full_name} is not signed"
-
return
-
end
-
end
-
-
opt = @opt
-
digester = Gem::Security::DIGEST_ALGORITHM
-
trust_dir = opt[:trust_dir]
-
time = Time.now
-
-
_, signer_digests = digests.find do |algorithm, file_digests|
-
file_digests.values.first.name == Gem::Security::DIGEST_NAME
-
end
-
-
if @verify_data
-
raise Gem::Security::Exception, 'no digests provided (probable bug)' if
-
signer_digests.nil? or signer_digests.empty?
-
else
-
signer_digests = {}
-
end
-
-
signer = chain.last
-
-
check_key signer, key if key
-
-
check_cert signer, nil, time if @verify_signer
-
-
check_chain chain, time if @verify_chain
-
-
check_root chain, time if @verify_root
-
-
if @only_trusted
-
check_trust chain, digester, trust_dir
-
elsif signatures.empty? and digests.empty?
-
# trust is irrelevant if there's no signatures to verify
-
else
-
alert_warning "#{subject signer} is not trusted for #{full_name}"
-
end
-
-
signatures.each do |file, _|
-
digest = signer_digests[file]
-
-
raise Gem::Security::Exception, "missing digest for #{file}" unless
-
digest
-
end
-
-
signer_digests.each do |file, digest|
-
signature = signatures[file]
-
-
raise Gem::Security::Exception, "missing signature for #{file}" unless
-
signature
-
-
check_data signer.public_key, digester, signature, digest if @verify_data
-
end
-
-
true
-
end
-
-
##
-
# Extracts the certificate chain from the +spec+ and calls #verify to ensure
-
# the signatures and certificate chain is valid according to the policy..
-
-
1
def verify_signatures(spec, digests, signatures)
-
chain = spec.cert_chain.map do |cert_pem|
-
OpenSSL::X509::Certificate.new cert_pem
-
end
-
-
verify chain, nil, digests, signatures, spec.full_name
-
-
true
-
end
-
-
1
alias to_s name # :nodoc:
-
-
end
-
# frozen_string_literal: true
-
##
-
# Basic OpenSSL-based package signing class.
-
-
1
require "rubygems/user_interaction"
-
-
1
class Gem::Security::Signer
-
-
1
include Gem::UserInteraction
-
-
##
-
# The chain of certificates for signing including the signing certificate
-
-
1
attr_accessor :cert_chain
-
-
##
-
# The private key for the signing certificate
-
-
1
attr_accessor :key
-
-
##
-
# The digest algorithm used to create the signature
-
-
1
attr_reader :digest_algorithm
-
-
##
-
# The name of the digest algorithm, used to pull digests out of the hash by
-
# name.
-
-
1
attr_reader :digest_name # :nodoc:
-
-
##
-
# Gem::Security::Signer options
-
-
1
attr_reader :options
-
-
1
DEFAULT_OPTIONS = {
-
expiration_length_days: 365
-
}.freeze
-
-
##
-
# Attemps to re-sign an expired cert with a given private key
-
1
def self.re_sign_cert(expired_cert, expired_cert_path, private_key)
-
return unless expired_cert.not_after < Time.now
-
-
expiry = expired_cert.not_after.strftime('%Y%m%d%H%M%S')
-
expired_cert_file = "#{File.basename(expired_cert_path)}.expired.#{expiry}"
-
new_expired_cert_path = File.join(Gem.user_home, ".gem", expired_cert_file)
-
-
Gem::Security.write(expired_cert, new_expired_cert_path)
-
-
re_signed_cert = Gem::Security.re_sign(
-
expired_cert,
-
private_key,
-
(Gem::Security::ONE_DAY * Gem.configuration.cert_expiration_length_days)
-
)
-
-
Gem::Security.write(re_signed_cert, expired_cert_path)
-
-
yield(expired_cert_path, new_expired_cert_path) if block_given?
-
end
-
-
##
-
# Creates a new signer with an RSA +key+ or path to a key, and a certificate
-
# +chain+ containing X509 certificates, encoding certificates or paths to
-
# certificates.
-
-
1
def initialize(key, cert_chain, passphrase = nil, options = {})
-
@cert_chain = cert_chain
-
@key = key
-
@passphrase = passphrase
-
@options = DEFAULT_OPTIONS.merge(options)
-
-
unless @key
-
default_key = File.join Gem.default_key_path
-
@key = default_key if File.exist? default_key
-
end
-
-
unless @cert_chain
-
default_cert = File.join Gem.default_cert_path
-
@cert_chain = [default_cert] if File.exist? default_cert
-
end
-
-
@digest_algorithm = Gem::Security::DIGEST_ALGORITHM
-
@digest_name = Gem::Security::DIGEST_NAME
-
-
if @key && !@key.is_a?(OpenSSL::PKey::RSA)
-
@key = OpenSSL::PKey::RSA.new(File.read(@key), @passphrase)
-
end
-
-
if @cert_chain
-
@cert_chain = @cert_chain.compact.map do |cert|
-
next cert if OpenSSL::X509::Certificate === cert
-
-
cert = File.read cert if File.exist? cert
-
-
OpenSSL::X509::Certificate.new cert
-
end
-
-
load_cert_chain
-
end
-
end
-
-
##
-
# Extracts the full name of +cert+. If the certificate has a subjectAltName
-
# this value is preferred, otherwise the subject is used.
-
-
1
def extract_name(cert) # :nodoc:
-
subject_alt_name = cert.extensions.find { |e| 'subjectAltName' == e.oid }
-
-
if subject_alt_name
-
/\Aemail:/ =~ subject_alt_name.value
-
-
$' || subject_alt_name.value
-
else
-
cert.subject
-
end
-
end
-
-
##
-
# Loads any missing issuers in the cert chain from the trusted certificates.
-
#
-
# If the issuer does not exist it is ignored as it will be checked later.
-
-
1
def load_cert_chain # :nodoc:
-
return if @cert_chain.empty?
-
-
while @cert_chain.first.issuer.to_s != @cert_chain.first.subject.to_s do
-
issuer = Gem::Security.trust_dir.issuer_of @cert_chain.first
-
-
break unless issuer # cert chain is verified later
-
-
@cert_chain.unshift issuer
-
end
-
end
-
-
##
-
# Sign data with given digest algorithm
-
-
1
def sign(data)
-
return unless @key
-
-
raise Gem::Security::Exception, 'no certs provided' if @cert_chain.empty?
-
-
if @cert_chain.length == 1 and @cert_chain.last.not_after < Time.now
-
alert("Your certificate has expired, trying to re-sign it...")
-
-
re_sign_key(
-
expiration_length: (Gem::Security::ONE_DAY * options[:expiration_length_days])
-
)
-
end
-
-
full_name = extract_name @cert_chain.last
-
-
Gem::Security::SigningPolicy.verify @cert_chain, @key, {}, {}, full_name
-
-
@key.sign @digest_algorithm.new, data
-
end
-
-
##
-
# Attempts to re-sign the private key if the signing certificate is expired.
-
#
-
# The key will be re-signed if:
-
# * The expired certificate is self-signed
-
# * The expired certificate is saved at ~/.gem/gem-public_cert.pem
-
# and the private key is saved at ~/.gem/gem-private_key.pem
-
# * There is no file matching the expiry date at
-
# ~/.gem/gem-public_cert.pem.expired.%Y%m%d%H%M%S
-
#
-
# If the signing certificate can be re-signed the expired certificate will
-
# be saved as ~/.gem/gem-public_cert.pem.expired.%Y%m%d%H%M%S where the
-
# expiry time (not after) is used for the timestamp.
-
-
1
def re_sign_key(expiration_length: Gem::Security::ONE_YEAR) # :nodoc:
-
old_cert = @cert_chain.last
-
-
disk_cert_path = File.join(Gem.default_cert_path)
-
disk_cert = File.read(disk_cert_path) rescue nil
-
-
disk_key_path = File.join(Gem.default_key_path)
-
disk_key =
-
OpenSSL::PKey::RSA.new(File.read(disk_key_path), @passphrase) rescue nil
-
-
return unless disk_key
-
-
if disk_key.to_pem == @key.to_pem && disk_cert == old_cert.to_pem
-
expiry = old_cert.not_after.strftime('%Y%m%d%H%M%S')
-
old_cert_file = "gem-public_cert.pem.expired.#{expiry}"
-
old_cert_path = File.join(Gem.user_home, ".gem", old_cert_file)
-
-
unless File.exist?(old_cert_path)
-
Gem::Security.write(old_cert, old_cert_path)
-
-
cert = Gem::Security.re_sign(old_cert, @key, expiration_length)
-
-
Gem::Security.write(cert, disk_cert_path)
-
-
alert("Your cert: #{disk_cert_path} has been auto re-signed with the key: #{disk_key_path}")
-
alert("Your expired cert will be located at: #{old_cert_path}")
-
-
@cert_chain = [cert]
-
end
-
end
-
end
-
-
end
-
# frozen_string_literal: true
-
##
-
# The TrustDir manages the trusted certificates for gem signature
-
# verification.
-
-
1
class Gem::Security::TrustDir
-
-
##
-
# Default permissions for the trust directory and its contents
-
-
1
DEFAULT_PERMISSIONS = {
-
:trust_dir => 0700,
-
:trusted_cert => 0600,
-
}.freeze
-
-
##
-
# The directory where trusted certificates will be stored.
-
-
1
attr_reader :dir
-
-
##
-
# Creates a new TrustDir using +dir+ where the directory and file
-
# permissions will be checked according to +permissions+
-
-
1
def initialize(dir, permissions = DEFAULT_PERMISSIONS)
-
@dir = dir
-
@permissions = permissions
-
-
@digester = Gem::Security::DIGEST_ALGORITHM
-
end
-
-
##
-
# Returns the path to the trusted +certificate+
-
-
1
def cert_path(certificate)
-
name_path certificate.subject
-
end
-
-
##
-
# Enumerates trusted certificates.
-
-
1
def each_certificate
-
return enum_for __method__ unless block_given?
-
-
glob = File.join @dir, '*.pem'
-
-
Dir[glob].each do |certificate_file|
-
begin
-
certificate = load_certificate certificate_file
-
-
yield certificate, certificate_file
-
rescue OpenSSL::X509::CertificateError
-
next # HACK warn
-
end
-
end
-
end
-
-
##
-
# Returns the issuer certificate of the given +certificate+ if it exists in
-
# the trust directory.
-
-
1
def issuer_of(certificate)
-
path = name_path certificate.issuer
-
-
return unless File.exist? path
-
-
load_certificate path
-
end
-
-
##
-
# Returns the path to the trusted certificate with the given ASN.1 +name+
-
-
1
def name_path(name)
-
digest = @digester.hexdigest name.to_s
-
-
File.join @dir, "cert-#{digest}.pem"
-
end
-
-
##
-
# Loads the given +certificate_file+
-
-
1
def load_certificate(certificate_file)
-
pem = File.read certificate_file
-
-
OpenSSL::X509::Certificate.new pem
-
end
-
-
##
-
# Add a certificate to trusted certificate list.
-
-
1
def trust_cert(certificate)
-
verify
-
-
destination = cert_path certificate
-
-
File.open destination, 'wb', 0600 do |io|
-
io.write certificate.to_pem
-
io.chmod(@permissions[:trusted_cert])
-
end
-
end
-
-
##
-
# Make sure the trust directory exists. If it does exist, make sure it's
-
# actually a directory. If not, then create it with the appropriate
-
# permissions.
-
-
1
def verify
-
if File.exist? @dir
-
raise Gem::Security::Exception,
-
"trust directory #{@dir} is not a directory" unless
-
File.directory? @dir
-
-
FileUtils.chmod 0700, @dir
-
else
-
FileUtils.mkdir_p @dir, :mode => @permissions[:trust_dir]
-
end
-
end
-
-
end
-
# frozen_string_literal: true
-
-
1
require 'socket.so'
-
1
require 'io/wait'
-
-
1
class Addrinfo
-
# creates an Addrinfo object from the arguments.
-
#
-
# The arguments are interpreted as similar to self.
-
#
-
# Addrinfo.tcp("0.0.0.0", 4649).family_addrinfo("www.ruby-lang.org", 80)
-
# #=> #<Addrinfo: 221.186.184.68:80 TCP (www.ruby-lang.org:80)>
-
#
-
# Addrinfo.unix("/tmp/sock").family_addrinfo("/tmp/sock2")
-
# #=> #<Addrinfo: /tmp/sock2 SOCK_STREAM>
-
#
-
1
def family_addrinfo(*args)
-
if args.empty?
-
raise ArgumentError, "no address specified"
-
elsif Addrinfo === args.first
-
raise ArgumentError, "too many arguments" if args.length != 1
-
addrinfo = args.first
-
if (self.pfamily != addrinfo.pfamily) ||
-
(self.socktype != addrinfo.socktype)
-
raise ArgumentError, "Addrinfo type mismatch"
-
end
-
addrinfo
-
elsif self.ip?
-
raise ArgumentError, "IP address needs host and port but #{args.length} arguments given" if args.length != 2
-
host, port = args
-
Addrinfo.getaddrinfo(host, port, self.pfamily, self.socktype, self.protocol)[0]
-
elsif self.unix?
-
raise ArgumentError, "UNIX socket needs single path argument but #{args.length} arguments given" if args.length != 1
-
path, = args
-
Addrinfo.unix(path)
-
else
-
raise ArgumentError, "unexpected family"
-
end
-
end
-
-
# creates a new Socket connected to the address of +local_addrinfo+.
-
#
-
# If _local_addrinfo_ is nil, the address of the socket is not bound.
-
#
-
# The _timeout_ specify the seconds for timeout.
-
# Errno::ETIMEDOUT is raised when timeout occur.
-
#
-
# If a block is given the created socket is yielded for each address.
-
#
-
1
def connect_internal(local_addrinfo, timeout=nil) # :yields: socket
-
sock = Socket.new(self.pfamily, self.socktype, self.protocol)
-
begin
-
sock.ipv6only! if self.ipv6?
-
sock.bind local_addrinfo if local_addrinfo
-
if timeout
-
case sock.connect_nonblock(self, exception: false)
-
when 0 # success or EISCONN, other errors raise
-
break
-
when :wait_writable
-
sock.wait_writable(timeout) or
-
raise Errno::ETIMEDOUT, 'user specified timeout'
-
end while true
-
else
-
sock.connect(self)
-
end
-
rescue Exception
-
sock.close
-
raise
-
end
-
if block_given?
-
begin
-
yield sock
-
ensure
-
sock.close
-
end
-
else
-
sock
-
end
-
end
-
1
protected :connect_internal
-
-
# :call-seq:
-
# addrinfo.connect_from([local_addr_args], [opts]) {|socket| ... }
-
# addrinfo.connect_from([local_addr_args], [opts])
-
#
-
# creates a socket connected to the address of self.
-
#
-
# If one or more arguments given as _local_addr_args_,
-
# it is used as the local address of the socket.
-
# _local_addr_args_ is given for family_addrinfo to obtain actual address.
-
#
-
# If _local_addr_args_ is not given, the local address of the socket is not bound.
-
#
-
# The optional last argument _opts_ is options represented by a hash.
-
# _opts_ may have following options:
-
#
-
# [:timeout] specify the timeout in seconds.
-
#
-
# If a block is given, it is called with the socket and the value of the block is returned.
-
# The socket is returned otherwise.
-
#
-
# Addrinfo.tcp("www.ruby-lang.org", 80).connect_from("0.0.0.0", 4649) {|s|
-
# s.print "GET / HTTP/1.0\r\nHost: www.ruby-lang.org\r\n\r\n"
-
# puts s.read
-
# }
-
#
-
# # Addrinfo object can be taken for the argument.
-
# Addrinfo.tcp("www.ruby-lang.org", 80).connect_from(Addrinfo.tcp("0.0.0.0", 4649)) {|s|
-
# s.print "GET / HTTP/1.0\r\nHost: www.ruby-lang.org\r\n\r\n"
-
# puts s.read
-
# }
-
#
-
1
def connect_from(*args, timeout: nil, &block)
-
connect_internal(family_addrinfo(*args), timeout, &block)
-
end
-
-
# :call-seq:
-
# addrinfo.connect([opts]) {|socket| ... }
-
# addrinfo.connect([opts])
-
#
-
# creates a socket connected to the address of self.
-
#
-
# The optional argument _opts_ is options represented by a hash.
-
# _opts_ may have following options:
-
#
-
# [:timeout] specify the timeout in seconds.
-
#
-
# If a block is given, it is called with the socket and the value of the block is returned.
-
# The socket is returned otherwise.
-
#
-
# Addrinfo.tcp("www.ruby-lang.org", 80).connect {|s|
-
# s.print "GET / HTTP/1.0\r\nHost: www.ruby-lang.org\r\n\r\n"
-
# puts s.read
-
# }
-
#
-
1
def connect(timeout: nil, &block)
-
connect_internal(nil, timeout, &block)
-
end
-
-
# :call-seq:
-
# addrinfo.connect_to([remote_addr_args], [opts]) {|socket| ... }
-
# addrinfo.connect_to([remote_addr_args], [opts])
-
#
-
# creates a socket connected to _remote_addr_args_ and bound to self.
-
#
-
# The optional last argument _opts_ is options represented by a hash.
-
# _opts_ may have following options:
-
#
-
# [:timeout] specify the timeout in seconds.
-
#
-
# If a block is given, it is called with the socket and the value of the block is returned.
-
# The socket is returned otherwise.
-
#
-
# Addrinfo.tcp("0.0.0.0", 4649).connect_to("www.ruby-lang.org", 80) {|s|
-
# s.print "GET / HTTP/1.0\r\nHost: www.ruby-lang.org\r\n\r\n"
-
# puts s.read
-
# }
-
#
-
1
def connect_to(*args, timeout: nil, &block)
-
remote_addrinfo = family_addrinfo(*args)
-
remote_addrinfo.connect_internal(self, timeout, &block)
-
end
-
-
# creates a socket bound to self.
-
#
-
# If a block is given, it is called with the socket and the value of the block is returned.
-
# The socket is returned otherwise.
-
#
-
# Addrinfo.udp("0.0.0.0", 9981).bind {|s|
-
# s.local_address.connect {|s| s.send "hello", 0 }
-
# p s.recv(10) #=> "hello"
-
# }
-
#
-
1
def bind
-
sock = Socket.new(self.pfamily, self.socktype, self.protocol)
-
begin
-
sock.ipv6only! if self.ipv6?
-
sock.setsockopt(:SOCKET, :REUSEADDR, 1)
-
sock.bind(self)
-
rescue Exception
-
sock.close
-
raise
-
end
-
if block_given?
-
begin
-
yield sock
-
ensure
-
sock.close
-
end
-
else
-
sock
-
end
-
end
-
-
# creates a listening socket bound to self.
-
1
def listen(backlog=Socket::SOMAXCONN)
-
sock = Socket.new(self.pfamily, self.socktype, self.protocol)
-
begin
-
sock.ipv6only! if self.ipv6?
-
sock.setsockopt(:SOCKET, :REUSEADDR, 1)
-
sock.bind(self)
-
sock.listen(backlog)
-
rescue Exception
-
sock.close
-
raise
-
end
-
if block_given?
-
begin
-
yield sock
-
ensure
-
sock.close
-
end
-
else
-
sock
-
end
-
end
-
-
# iterates over the list of Addrinfo objects obtained by Addrinfo.getaddrinfo.
-
#
-
# Addrinfo.foreach(nil, 80) {|x| p x }
-
# #=> #<Addrinfo: 127.0.0.1:80 TCP (:80)>
-
# # #<Addrinfo: 127.0.0.1:80 UDP (:80)>
-
# # #<Addrinfo: [::1]:80 TCP (:80)>
-
# # #<Addrinfo: [::1]:80 UDP (:80)>
-
#
-
1
def self.foreach(nodename, service, family=nil, socktype=nil, protocol=nil, flags=nil, timeout: nil, &block)
-
Addrinfo.getaddrinfo(nodename, service, family, socktype, protocol, flags, timeout: timeout).each(&block)
-
end
-
end
-
-
1
class BasicSocket < IO
-
# Returns an address of the socket suitable for connect in the local machine.
-
#
-
# This method returns _self_.local_address, except following condition.
-
#
-
# - IPv4 unspecified address (0.0.0.0) is replaced by IPv4 loopback address (127.0.0.1).
-
# - IPv6 unspecified address (::) is replaced by IPv6 loopback address (::1).
-
#
-
# If the local address is not suitable for connect, SocketError is raised.
-
# IPv4 and IPv6 address which port is 0 is not suitable for connect.
-
# Unix domain socket which has no path is not suitable for connect.
-
#
-
# Addrinfo.tcp("0.0.0.0", 0).listen {|serv|
-
# p serv.connect_address #=> #<Addrinfo: 127.0.0.1:53660 TCP>
-
# serv.connect_address.connect {|c|
-
# s, _ = serv.accept
-
# p [c, s] #=> [#<Socket:fd 4>, #<Socket:fd 6>]
-
# }
-
# }
-
#
-
1
def connect_address
-
addr = local_address
-
afamily = addr.afamily
-
if afamily == Socket::AF_INET
-
raise SocketError, "unbound IPv4 socket" if addr.ip_port == 0
-
if addr.ip_address == "0.0.0.0"
-
addr = Addrinfo.new(["AF_INET", addr.ip_port, nil, "127.0.0.1"], addr.pfamily, addr.socktype, addr.protocol)
-
end
-
elsif defined?(Socket::AF_INET6) && afamily == Socket::AF_INET6
-
raise SocketError, "unbound IPv6 socket" if addr.ip_port == 0
-
if addr.ip_address == "::"
-
addr = Addrinfo.new(["AF_INET6", addr.ip_port, nil, "::1"], addr.pfamily, addr.socktype, addr.protocol)
-
elsif addr.ip_address == "0.0.0.0" # MacOS X 10.4 returns "a.b.c.d" for IPv4-mapped IPv6 address.
-
addr = Addrinfo.new(["AF_INET6", addr.ip_port, nil, "::1"], addr.pfamily, addr.socktype, addr.protocol)
-
elsif addr.ip_address == "::ffff:0.0.0.0" # MacOS X 10.6 returns "::ffff:a.b.c.d" for IPv4-mapped IPv6 address.
-
addr = Addrinfo.new(["AF_INET6", addr.ip_port, nil, "::1"], addr.pfamily, addr.socktype, addr.protocol)
-
end
-
elsif defined?(Socket::AF_UNIX) && afamily == Socket::AF_UNIX
-
raise SocketError, "unbound Unix socket" if addr.unix_path == ""
-
end
-
addr
-
end
-
-
# call-seq:
-
# basicsocket.sendmsg(mesg, flags=0, dest_sockaddr=nil, *controls) => numbytes_sent
-
#
-
# sendmsg sends a message using sendmsg(2) system call in blocking manner.
-
#
-
# _mesg_ is a string to send.
-
#
-
# _flags_ is bitwise OR of MSG_* constants such as Socket::MSG_OOB.
-
#
-
# _dest_sockaddr_ is a destination socket address for connection-less socket.
-
# It should be a sockaddr such as a result of Socket.sockaddr_in.
-
# An Addrinfo object can be used too.
-
#
-
# _controls_ is a list of ancillary data.
-
# The element of _controls_ should be Socket::AncillaryData or
-
# 3-elements array.
-
# The 3-element array should contains cmsg_level, cmsg_type and data.
-
#
-
# The return value, _numbytes_sent_ is an integer which is the number of bytes sent.
-
#
-
# sendmsg can be used to implement send_io as follows:
-
#
-
# # use Socket::AncillaryData.
-
# ancdata = Socket::AncillaryData.int(:UNIX, :SOCKET, :RIGHTS, io.fileno)
-
# sock.sendmsg("a", 0, nil, ancdata)
-
#
-
# # use 3-element array.
-
# ancdata = [:SOCKET, :RIGHTS, [io.fileno].pack("i!")]
-
# sock.sendmsg("\0", 0, nil, ancdata)
-
1
def sendmsg(mesg, flags = 0, dest_sockaddr = nil, *controls)
-
__sendmsg(mesg, flags, dest_sockaddr, controls)
-
end
-
-
# call-seq:
-
# basicsocket.sendmsg_nonblock(mesg, flags=0, dest_sockaddr=nil, *controls, opts={}) => numbytes_sent
-
#
-
# sendmsg_nonblock sends a message using sendmsg(2) system call in non-blocking manner.
-
#
-
# It is similar to BasicSocket#sendmsg
-
# but the non-blocking flag is set before the system call
-
# and it doesn't retry the system call.
-
#
-
# By specifying a keyword argument _exception_ to +false+, you can indicate
-
# that sendmsg_nonblock should not raise an IO::WaitWritable exception, but
-
# return the symbol +:wait_writable+ instead.
-
1
def sendmsg_nonblock(mesg, flags = 0, dest_sockaddr = nil, *controls,
-
exception: true)
-
__sendmsg_nonblock(mesg, flags, dest_sockaddr, controls, exception)
-
end
-
-
# call-seq:
-
# basicsocket.recv_nonblock(maxlen [, flags [, buf [, options ]]]) => mesg
-
#
-
# Receives up to _maxlen_ bytes from +socket+ using recvfrom(2) after
-
# O_NONBLOCK is set for the underlying file descriptor.
-
# _flags_ is zero or more of the +MSG_+ options.
-
# The result, _mesg_, is the data received.
-
#
-
# When recvfrom(2) returns 0, Socket#recv_nonblock returns
-
# an empty string as data.
-
# The meaning depends on the socket: EOF on TCP, empty packet on UDP, etc.
-
#
-
# === Parameters
-
# * +maxlen+ - the number of bytes to receive from the socket
-
# * +flags+ - zero or more of the +MSG_+ options
-
# * +buf+ - destination String buffer
-
# * +options+ - keyword hash, supporting `exception: false`
-
#
-
# === Example
-
# serv = TCPServer.new("127.0.0.1", 0)
-
# af, port, host, addr = serv.addr
-
# c = TCPSocket.new(addr, port)
-
# s = serv.accept
-
# c.send "aaa", 0
-
# begin # emulate blocking recv.
-
# p s.recv_nonblock(10) #=> "aaa"
-
# rescue IO::WaitReadable
-
# IO.select([s])
-
# retry
-
# end
-
#
-
# Refer to Socket#recvfrom for the exceptions that may be thrown if the call
-
# to _recv_nonblock_ fails.
-
#
-
# BasicSocket#recv_nonblock may raise any error corresponding to recvfrom(2) failure,
-
# including Errno::EWOULDBLOCK.
-
#
-
# If the exception is Errno::EWOULDBLOCK or Errno::EAGAIN,
-
# it is extended by IO::WaitReadable.
-
# So IO::WaitReadable can be used to rescue the exceptions for retrying recv_nonblock.
-
#
-
# By specifying a keyword argument _exception_ to +false+, you can indicate
-
# that recv_nonblock should not raise an IO::WaitReadable exception, but
-
# return the symbol +:wait_readable+ instead.
-
#
-
# === See
-
# * Socket#recvfrom
-
1
def recv_nonblock(len, flag = 0, str = nil, exception: true)
-
__recv_nonblock(len, flag, str, exception)
-
end
-
-
# call-seq:
-
# basicsocket.recvmsg(maxmesglen=nil, flags=0, maxcontrollen=nil, opts={}) => [mesg, sender_addrinfo, rflags, *controls]
-
#
-
# recvmsg receives a message using recvmsg(2) system call in blocking manner.
-
#
-
# _maxmesglen_ is the maximum length of mesg to receive.
-
#
-
# _flags_ is bitwise OR of MSG_* constants such as Socket::MSG_PEEK.
-
#
-
# _maxcontrollen_ is the maximum length of controls (ancillary data) to receive.
-
#
-
# _opts_ is option hash.
-
# Currently :scm_rights=>bool is the only option.
-
#
-
# :scm_rights option specifies that application expects SCM_RIGHTS control message.
-
# If the value is nil or false, application don't expects SCM_RIGHTS control message.
-
# In this case, recvmsg closes the passed file descriptors immediately.
-
# This is the default behavior.
-
#
-
# If :scm_rights value is neither nil nor false, application expects SCM_RIGHTS control message.
-
# In this case, recvmsg creates IO objects for each file descriptors for
-
# Socket::AncillaryData#unix_rights method.
-
#
-
# The return value is 4-elements array.
-
#
-
# _mesg_ is a string of the received message.
-
#
-
# _sender_addrinfo_ is a sender socket address for connection-less socket.
-
# It is an Addrinfo object.
-
# For connection-oriented socket such as TCP, sender_addrinfo is platform dependent.
-
#
-
# _rflags_ is a flags on the received message which is bitwise OR of MSG_* constants such as Socket::MSG_TRUNC.
-
# It will be nil if the system uses 4.3BSD style old recvmsg system call.
-
#
-
# _controls_ is ancillary data which is an array of Socket::AncillaryData objects such as:
-
#
-
# #<Socket::AncillaryData: AF_UNIX SOCKET RIGHTS 7>
-
#
-
# _maxmesglen_ and _maxcontrollen_ can be nil.
-
# In that case, the buffer will be grown until the message is not truncated.
-
# Internally, MSG_PEEK is used.
-
# Buffer full and MSG_CTRUNC are checked for truncation.
-
#
-
# recvmsg can be used to implement recv_io as follows:
-
#
-
# mesg, sender_sockaddr, rflags, *controls = sock.recvmsg(:scm_rights=>true)
-
# controls.each {|ancdata|
-
# if ancdata.cmsg_is?(:SOCKET, :RIGHTS)
-
# return ancdata.unix_rights[0]
-
# end
-
# }
-
1
def recvmsg(dlen = nil, flags = 0, clen = nil, scm_rights: false)
-
__recvmsg(dlen, flags, clen, scm_rights)
-
end
-
-
# call-seq:
-
# basicsocket.recvmsg_nonblock(maxdatalen=nil, flags=0, maxcontrollen=nil, opts={}) => [data, sender_addrinfo, rflags, *controls]
-
#
-
# recvmsg receives a message using recvmsg(2) system call in non-blocking manner.
-
#
-
# It is similar to BasicSocket#recvmsg
-
# but non-blocking flag is set before the system call
-
# and it doesn't retry the system call.
-
#
-
# By specifying a keyword argument _exception_ to +false+, you can indicate
-
# that recvmsg_nonblock should not raise an IO::WaitReadable exception, but
-
# return the symbol +:wait_readable+ instead.
-
1
def recvmsg_nonblock(dlen = nil, flags = 0, clen = nil,
-
scm_rights: false, exception: true)
-
__recvmsg_nonblock(dlen, flags, clen, scm_rights, exception)
-
end
-
-
# Linux-specific optimizations to avoid fcntl for IO#read_nonblock
-
# and IO#write_nonblock using MSG_DONTWAIT
-
# Do other platforms support MSG_DONTWAIT reliably?
-
1
if RUBY_PLATFORM =~ /linux/ && Socket.const_defined?(:MSG_DONTWAIT)
-
1
def read_nonblock(len, str = nil, exception: true) # :nodoc:
-
3
__read_nonblock(len, str, exception)
-
end
-
-
1
def write_nonblock(buf, exception: true) # :nodoc:
-
6
__write_nonblock(buf, exception)
-
end
-
end
-
end
-
-
1
class Socket < BasicSocket
-
# enable the socket option IPV6_V6ONLY if IPV6_V6ONLY is available.
-
1
def ipv6only!
-
if defined? Socket::IPV6_V6ONLY
-
self.setsockopt(:IPV6, :V6ONLY, 1)
-
end
-
end
-
-
# call-seq:
-
# socket.recvfrom_nonblock(maxlen[, flags[, outbuf[, opts]]]) => [mesg, sender_addrinfo]
-
#
-
# Receives up to _maxlen_ bytes from +socket+ using recvfrom(2) after
-
# O_NONBLOCK is set for the underlying file descriptor.
-
# _flags_ is zero or more of the +MSG_+ options.
-
# The first element of the results, _mesg_, is the data received.
-
# The second element, _sender_addrinfo_, contains protocol-specific address
-
# information of the sender.
-
#
-
# When recvfrom(2) returns 0, Socket#recvfrom_nonblock returns
-
# an empty string as data.
-
# The meaning depends on the socket: EOF on TCP, empty packet on UDP, etc.
-
#
-
# === Parameters
-
# * +maxlen+ - the maximum number of bytes to receive from the socket
-
# * +flags+ - zero or more of the +MSG_+ options
-
# * +outbuf+ - destination String buffer
-
# * +opts+ - keyword hash, supporting `exception: false`
-
#
-
# === Example
-
# # In one file, start this first
-
# require 'socket'
-
# include Socket::Constants
-
# socket = Socket.new(AF_INET, SOCK_STREAM, 0)
-
# sockaddr = Socket.sockaddr_in(2200, 'localhost')
-
# socket.bind(sockaddr)
-
# socket.listen(5)
-
# client, client_addrinfo = socket.accept
-
# begin # emulate blocking recvfrom
-
# pair = client.recvfrom_nonblock(20)
-
# rescue IO::WaitReadable
-
# IO.select([client])
-
# retry
-
# end
-
# data = pair[0].chomp
-
# puts "I only received 20 bytes '#{data}'"
-
# sleep 1
-
# socket.close
-
#
-
# # In another file, start this second
-
# require 'socket'
-
# include Socket::Constants
-
# socket = Socket.new(AF_INET, SOCK_STREAM, 0)
-
# sockaddr = Socket.sockaddr_in(2200, 'localhost')
-
# socket.connect(sockaddr)
-
# socket.puts "Watch this get cut short!"
-
# socket.close
-
#
-
# Refer to Socket#recvfrom for the exceptions that may be thrown if the call
-
# to _recvfrom_nonblock_ fails.
-
#
-
# Socket#recvfrom_nonblock may raise any error corresponding to recvfrom(2) failure,
-
# including Errno::EWOULDBLOCK.
-
#
-
# If the exception is Errno::EWOULDBLOCK or Errno::EAGAIN,
-
# it is extended by IO::WaitReadable.
-
# So IO::WaitReadable can be used to rescue the exceptions for retrying
-
# recvfrom_nonblock.
-
#
-
# By specifying a keyword argument _exception_ to +false+, you can indicate
-
# that recvfrom_nonblock should not raise an IO::WaitReadable exception, but
-
# return the symbol +:wait_readable+ instead.
-
#
-
# === See
-
# * Socket#recvfrom
-
1
def recvfrom_nonblock(len, flag = 0, str = nil, exception: true)
-
__recvfrom_nonblock(len, flag, str, exception)
-
end
-
-
# call-seq:
-
# socket.accept_nonblock([options]) => [client_socket, client_addrinfo]
-
#
-
# Accepts an incoming connection using accept(2) after
-
# O_NONBLOCK is set for the underlying file descriptor.
-
# It returns an array containing the accepted socket
-
# for the incoming connection, _client_socket_,
-
# and an Addrinfo, _client_addrinfo_.
-
#
-
# === Example
-
# # In one script, start this first
-
# require 'socket'
-
# include Socket::Constants
-
# socket = Socket.new(AF_INET, SOCK_STREAM, 0)
-
# sockaddr = Socket.sockaddr_in(2200, 'localhost')
-
# socket.bind(sockaddr)
-
# socket.listen(5)
-
# begin # emulate blocking accept
-
# client_socket, client_addrinfo = socket.accept_nonblock
-
# rescue IO::WaitReadable, Errno::EINTR
-
# IO.select([socket])
-
# retry
-
# end
-
# puts "The client said, '#{client_socket.readline.chomp}'"
-
# client_socket.puts "Hello from script one!"
-
# socket.close
-
#
-
# # In another script, start this second
-
# require 'socket'
-
# include Socket::Constants
-
# socket = Socket.new(AF_INET, SOCK_STREAM, 0)
-
# sockaddr = Socket.sockaddr_in(2200, 'localhost')
-
# socket.connect(sockaddr)
-
# socket.puts "Hello from script 2."
-
# puts "The server said, '#{socket.readline.chomp}'"
-
# socket.close
-
#
-
# Refer to Socket#accept for the exceptions that may be thrown if the call
-
# to _accept_nonblock_ fails.
-
#
-
# Socket#accept_nonblock may raise any error corresponding to accept(2) failure,
-
# including Errno::EWOULDBLOCK.
-
#
-
# If the exception is Errno::EWOULDBLOCK, Errno::EAGAIN, Errno::ECONNABORTED or Errno::EPROTO,
-
# it is extended by IO::WaitReadable.
-
# So IO::WaitReadable can be used to rescue the exceptions for retrying accept_nonblock.
-
#
-
# By specifying a keyword argument _exception_ to +false+, you can indicate
-
# that accept_nonblock should not raise an IO::WaitReadable exception, but
-
# return the symbol +:wait_readable+ instead.
-
#
-
# === See
-
# * Socket#accept
-
1
def accept_nonblock(exception: true)
-
__accept_nonblock(exception)
-
end
-
-
# :call-seq:
-
# Socket.tcp(host, port, local_host=nil, local_port=nil, [opts]) {|socket| ... }
-
# Socket.tcp(host, port, local_host=nil, local_port=nil, [opts])
-
#
-
# creates a new socket object connected to host:port using TCP/IP.
-
#
-
# If local_host:local_port is given,
-
# the socket is bound to it.
-
#
-
# The optional last argument _opts_ is options represented by a hash.
-
# _opts_ may have following options:
-
#
-
# [:connect_timeout] specify the timeout in seconds.
-
# [:resolv_timeout] specify the name resolution timeout in seconds.
-
#
-
# If a block is given, the block is called with the socket.
-
# The value of the block is returned.
-
# The socket is closed when this method returns.
-
#
-
# If no block is given, the socket is returned.
-
#
-
# Socket.tcp("www.ruby-lang.org", 80) {|sock|
-
# sock.print "GET / HTTP/1.0\r\nHost: www.ruby-lang.org\r\n\r\n"
-
# sock.close_write
-
# puts sock.read
-
# }
-
#
-
1
def self.tcp(host, port, local_host = nil, local_port = nil, connect_timeout: nil, resolv_timeout: nil) # :yield: socket
-
last_error = nil
-
ret = nil
-
-
local_addr_list = nil
-
if local_host != nil || local_port != nil
-
local_addr_list = Addrinfo.getaddrinfo(local_host, local_port, nil, :STREAM, nil)
-
end
-
-
Addrinfo.foreach(host, port, nil, :STREAM, timeout: resolv_timeout) {|ai|
-
if local_addr_list
-
local_addr = local_addr_list.find {|local_ai| local_ai.afamily == ai.afamily }
-
next unless local_addr
-
else
-
local_addr = nil
-
end
-
begin
-
sock = local_addr ?
-
ai.connect_from(local_addr, timeout: connect_timeout) :
-
ai.connect(timeout: connect_timeout)
-
rescue SystemCallError
-
last_error = $!
-
next
-
end
-
ret = sock
-
break
-
}
-
unless ret
-
if last_error
-
raise last_error
-
else
-
raise SocketError, "no appropriate local address"
-
end
-
end
-
if block_given?
-
begin
-
yield ret
-
ensure
-
ret.close
-
end
-
else
-
ret
-
end
-
end
-
-
# :stopdoc:
-
1
def self.ip_sockets_port0(ai_list, reuseaddr)
-
sockets = []
-
begin
-
sockets.clear
-
port = nil
-
ai_list.each {|ai|
-
begin
-
s = Socket.new(ai.pfamily, ai.socktype, ai.protocol)
-
rescue SystemCallError
-
next
-
end
-
sockets << s
-
s.ipv6only! if ai.ipv6?
-
if reuseaddr
-
s.setsockopt(:SOCKET, :REUSEADDR, 1)
-
end
-
unless port
-
s.bind(ai)
-
port = s.local_address.ip_port
-
else
-
s.bind(ai.family_addrinfo(ai.ip_address, port))
-
end
-
}
-
rescue Errno::EADDRINUSE
-
sockets.each(&:close)
-
retry
-
rescue Exception
-
sockets.each(&:close)
-
raise
-
end
-
sockets
-
end
-
1
class << self
-
1
private :ip_sockets_port0
-
end
-
-
1
def self.tcp_server_sockets_port0(host)
-
ai_list = Addrinfo.getaddrinfo(host, 0, nil, :STREAM, nil, Socket::AI_PASSIVE)
-
sockets = ip_sockets_port0(ai_list, true)
-
begin
-
sockets.each {|s|
-
s.listen(Socket::SOMAXCONN)
-
}
-
rescue Exception
-
sockets.each(&:close)
-
raise
-
end
-
sockets
-
end
-
1
class << self
-
1
private :tcp_server_sockets_port0
-
end
-
# :startdoc:
-
-
# creates TCP/IP server sockets for _host_ and _port_.
-
# _host_ is optional.
-
#
-
# If no block given,
-
# it returns an array of listening sockets.
-
#
-
# If a block is given, the block is called with the sockets.
-
# The value of the block is returned.
-
# The socket is closed when this method returns.
-
#
-
# If _port_ is 0, actual port number is chosen dynamically.
-
# However all sockets in the result has same port number.
-
#
-
# # tcp_server_sockets returns two sockets.
-
# sockets = Socket.tcp_server_sockets(1296)
-
# p sockets #=> [#<Socket:fd 3>, #<Socket:fd 4>]
-
#
-
# # The sockets contains IPv6 and IPv4 sockets.
-
# sockets.each {|s| p s.local_address }
-
# #=> #<Addrinfo: [::]:1296 TCP>
-
# # #<Addrinfo: 0.0.0.0:1296 TCP>
-
#
-
# # IPv6 and IPv4 socket has same port number, 53114, even if it is chosen dynamically.
-
# sockets = Socket.tcp_server_sockets(0)
-
# sockets.each {|s| p s.local_address }
-
# #=> #<Addrinfo: [::]:53114 TCP>
-
# # #<Addrinfo: 0.0.0.0:53114 TCP>
-
#
-
# # The block is called with the sockets.
-
# Socket.tcp_server_sockets(0) {|sockets|
-
# p sockets #=> [#<Socket:fd 3>, #<Socket:fd 4>]
-
# }
-
#
-
1
def self.tcp_server_sockets(host=nil, port)
-
if port == 0
-
sockets = tcp_server_sockets_port0(host)
-
else
-
last_error = nil
-
sockets = []
-
begin
-
Addrinfo.foreach(host, port, nil, :STREAM, nil, Socket::AI_PASSIVE) {|ai|
-
begin
-
s = ai.listen
-
rescue SystemCallError
-
last_error = $!
-
next
-
end
-
sockets << s
-
}
-
if sockets.empty?
-
raise last_error
-
end
-
rescue Exception
-
sockets.each(&:close)
-
raise
-
end
-
end
-
if block_given?
-
begin
-
yield sockets
-
ensure
-
sockets.each(&:close)
-
end
-
else
-
sockets
-
end
-
end
-
-
# yield socket and client address for each a connection accepted via given sockets.
-
#
-
# The arguments are a list of sockets.
-
# The individual argument should be a socket or an array of sockets.
-
#
-
# This method yields the block sequentially.
-
# It means that the next connection is not accepted until the block returns.
-
# So concurrent mechanism, thread for example, should be used to service multiple clients at a time.
-
#
-
1
def self.accept_loop(*sockets) # :yield: socket, client_addrinfo
-
sockets.flatten!(1)
-
if sockets.empty?
-
raise ArgumentError, "no sockets"
-
end
-
loop {
-
readable, _, _ = IO.select(sockets)
-
readable.each {|r|
-
sock, addr = r.accept_nonblock(exception: false)
-
next if sock == :wait_readable
-
yield sock, addr
-
}
-
}
-
end
-
-
# creates a TCP/IP server on _port_ and calls the block for each connection accepted.
-
# The block is called with a socket and a client_address as an Addrinfo object.
-
#
-
# If _host_ is specified, it is used with _port_ to determine the server addresses.
-
#
-
# The socket is *not* closed when the block returns.
-
# So application should close it explicitly.
-
#
-
# This method calls the block sequentially.
-
# It means that the next connection is not accepted until the block returns.
-
# So concurrent mechanism, thread for example, should be used to service multiple clients at a time.
-
#
-
# Note that Addrinfo.getaddrinfo is used to determine the server socket addresses.
-
# When Addrinfo.getaddrinfo returns two or more addresses,
-
# IPv4 and IPv6 address for example,
-
# all of them are used.
-
# Socket.tcp_server_loop succeeds if one socket can be used at least.
-
#
-
# # Sequential echo server.
-
# # It services only one client at a time.
-
# Socket.tcp_server_loop(16807) {|sock, client_addrinfo|
-
# begin
-
# IO.copy_stream(sock, sock)
-
# ensure
-
# sock.close
-
# end
-
# }
-
#
-
# # Threaded echo server
-
# # It services multiple clients at a time.
-
# # Note that it may accept connections too much.
-
# Socket.tcp_server_loop(16807) {|sock, client_addrinfo|
-
# Thread.new {
-
# begin
-
# IO.copy_stream(sock, sock)
-
# ensure
-
# sock.close
-
# end
-
# }
-
# }
-
#
-
1
def self.tcp_server_loop(host=nil, port, &b) # :yield: socket, client_addrinfo
-
tcp_server_sockets(host, port) {|sockets|
-
accept_loop(sockets, &b)
-
}
-
end
-
-
# :call-seq:
-
# Socket.udp_server_sockets([host, ] port)
-
#
-
# Creates UDP/IP sockets for a UDP server.
-
#
-
# If no block given, it returns an array of sockets.
-
#
-
# If a block is given, the block is called with the sockets.
-
# The value of the block is returned.
-
# The sockets are closed when this method returns.
-
#
-
# If _port_ is zero, some port is chosen.
-
# But the chosen port is used for the all sockets.
-
#
-
# # UDP/IP echo server
-
# Socket.udp_server_sockets(0) {|sockets|
-
# p sockets.first.local_address.ip_port #=> 32963
-
# Socket.udp_server_loop_on(sockets) {|msg, msg_src|
-
# msg_src.reply msg
-
# }
-
# }
-
#
-
1
def self.udp_server_sockets(host=nil, port)
-
last_error = nil
-
sockets = []
-
-
ipv6_recvpktinfo = nil
-
if defined? Socket::AncillaryData
-
if defined? Socket::IPV6_RECVPKTINFO # RFC 3542
-
ipv6_recvpktinfo = Socket::IPV6_RECVPKTINFO
-
elsif defined? Socket::IPV6_PKTINFO # RFC 2292
-
ipv6_recvpktinfo = Socket::IPV6_PKTINFO
-
end
-
end
-
-
local_addrs = Socket.ip_address_list
-
-
ip_list = []
-
Addrinfo.foreach(host, port, nil, :DGRAM, nil, Socket::AI_PASSIVE) {|ai|
-
if ai.ipv4? && ai.ip_address == "0.0.0.0"
-
local_addrs.each {|a|
-
next unless a.ipv4?
-
ip_list << Addrinfo.new(a.to_sockaddr, :INET, :DGRAM, 0);
-
}
-
elsif ai.ipv6? && ai.ip_address == "::" && !ipv6_recvpktinfo
-
local_addrs.each {|a|
-
next unless a.ipv6?
-
ip_list << Addrinfo.new(a.to_sockaddr, :INET6, :DGRAM, 0);
-
}
-
else
-
ip_list << ai
-
end
-
}
-
ip_list.uniq!(&:to_sockaddr)
-
-
if port == 0
-
sockets = ip_sockets_port0(ip_list, false)
-
else
-
ip_list.each {|ip|
-
ai = Addrinfo.udp(ip.ip_address, port)
-
begin
-
s = ai.bind
-
rescue SystemCallError
-
last_error = $!
-
next
-
end
-
sockets << s
-
}
-
if sockets.empty?
-
raise last_error
-
end
-
end
-
-
sockets.each {|s|
-
ai = s.local_address
-
if ipv6_recvpktinfo && ai.ipv6? && ai.ip_address == "::"
-
s.setsockopt(:IPV6, ipv6_recvpktinfo, 1)
-
end
-
}
-
-
if block_given?
-
begin
-
yield sockets
-
ensure
-
sockets.each(&:close) if sockets
-
end
-
else
-
sockets
-
end
-
end
-
-
# :call-seq:
-
# Socket.udp_server_recv(sockets) {|msg, msg_src| ... }
-
#
-
# Receive UDP/IP packets from the given _sockets_.
-
# For each packet received, the block is called.
-
#
-
# The block receives _msg_ and _msg_src_.
-
# _msg_ is a string which is the payload of the received packet.
-
# _msg_src_ is a Socket::UDPSource object which is used for reply.
-
#
-
# Socket.udp_server_loop can be implemented using this method as follows.
-
#
-
# udp_server_sockets(host, port) {|sockets|
-
# loop {
-
# readable, _, _ = IO.select(sockets)
-
# udp_server_recv(readable) {|msg, msg_src| ... }
-
# }
-
# }
-
#
-
1
def self.udp_server_recv(sockets)
-
sockets.each {|r|
-
msg, sender_addrinfo, _, *controls = r.recvmsg_nonblock(exception: false)
-
next if msg == :wait_readable
-
ai = r.local_address
-
if ai.ipv6? and pktinfo = controls.find {|c| c.cmsg_is?(:IPV6, :PKTINFO) }
-
ai = Addrinfo.udp(pktinfo.ipv6_pktinfo_addr.ip_address, ai.ip_port)
-
yield msg, UDPSource.new(sender_addrinfo, ai) {|reply_msg|
-
r.sendmsg reply_msg, 0, sender_addrinfo, pktinfo
-
}
-
else
-
yield msg, UDPSource.new(sender_addrinfo, ai) {|reply_msg|
-
r.send reply_msg, 0, sender_addrinfo
-
}
-
end
-
}
-
end
-
-
# :call-seq:
-
# Socket.udp_server_loop_on(sockets) {|msg, msg_src| ... }
-
#
-
# Run UDP/IP server loop on the given sockets.
-
#
-
# The return value of Socket.udp_server_sockets is appropriate for the argument.
-
#
-
# It calls the block for each message received.
-
#
-
1
def self.udp_server_loop_on(sockets, &b) # :yield: msg, msg_src
-
loop {
-
readable, _, _ = IO.select(sockets)
-
udp_server_recv(readable, &b)
-
}
-
end
-
-
# :call-seq:
-
# Socket.udp_server_loop(port) {|msg, msg_src| ... }
-
# Socket.udp_server_loop(host, port) {|msg, msg_src| ... }
-
#
-
# creates a UDP/IP server on _port_ and calls the block for each message arrived.
-
# The block is called with the message and its source information.
-
#
-
# This method allocates sockets internally using _port_.
-
# If _host_ is specified, it is used conjunction with _port_ to determine the server addresses.
-
#
-
# The _msg_ is a string.
-
#
-
# The _msg_src_ is a Socket::UDPSource object.
-
# It is used for reply.
-
#
-
# # UDP/IP echo server.
-
# Socket.udp_server_loop(9261) {|msg, msg_src|
-
# msg_src.reply msg
-
# }
-
#
-
1
def self.udp_server_loop(host=nil, port, &b) # :yield: message, message_source
-
udp_server_sockets(host, port) {|sockets|
-
udp_server_loop_on(sockets, &b)
-
}
-
end
-
-
# UDP/IP address information used by Socket.udp_server_loop.
-
1
class UDPSource
-
# +remote_address+ is an Addrinfo object.
-
#
-
# +local_address+ is an Addrinfo object.
-
#
-
# +reply_proc+ is a Proc used to send reply back to the source.
-
1
def initialize(remote_address, local_address, &reply_proc)
-
@remote_address = remote_address
-
@local_address = local_address
-
@reply_proc = reply_proc
-
end
-
-
# Address of the source
-
1
attr_reader :remote_address
-
-
# Local address
-
1
attr_reader :local_address
-
-
1
def inspect # :nodoc:
-
"\#<#{self.class}: #{@remote_address.inspect_sockaddr} to #{@local_address.inspect_sockaddr}>".dup
-
end
-
-
# Sends the String +msg+ to the source
-
1
def reply(msg)
-
@reply_proc.call msg
-
end
-
end
-
-
# creates a new socket connected to path using UNIX socket socket.
-
#
-
# If a block is given, the block is called with the socket.
-
# The value of the block is returned.
-
# The socket is closed when this method returns.
-
#
-
# If no block is given, the socket is returned.
-
#
-
# # talk to /tmp/sock socket.
-
# Socket.unix("/tmp/sock") {|sock|
-
# t = Thread.new { IO.copy_stream(sock, STDOUT) }
-
# IO.copy_stream(STDIN, sock)
-
# t.join
-
# }
-
#
-
1
def self.unix(path) # :yield: socket
-
addr = Addrinfo.unix(path)
-
sock = addr.connect
-
if block_given?
-
begin
-
yield sock
-
ensure
-
sock.close
-
end
-
else
-
sock
-
end
-
end
-
-
# creates a UNIX server socket on _path_
-
#
-
# If no block given, it returns a listening socket.
-
#
-
# If a block is given, it is called with the socket and the block value is returned.
-
# When the block exits, the socket is closed and the socket file is removed.
-
#
-
# socket = Socket.unix_server_socket("/tmp/s")
-
# p socket #=> #<Socket:fd 3>
-
# p socket.local_address #=> #<Addrinfo: /tmp/s SOCK_STREAM>
-
#
-
# Socket.unix_server_socket("/tmp/sock") {|s|
-
# p s #=> #<Socket:fd 3>
-
# p s.local_address #=> # #<Addrinfo: /tmp/sock SOCK_STREAM>
-
# }
-
#
-
1
def self.unix_server_socket(path)
-
unless unix_socket_abstract_name?(path)
-
begin
-
st = File.lstat(path)
-
rescue Errno::ENOENT
-
end
-
if st&.socket? && st.owned?
-
File.unlink path
-
end
-
end
-
s = Addrinfo.unix(path).listen
-
if block_given?
-
begin
-
yield s
-
ensure
-
s.close
-
unless unix_socket_abstract_name?(path)
-
File.unlink path
-
end
-
end
-
else
-
s
-
end
-
end
-
-
1
class << self
-
1
private
-
-
1
def unix_socket_abstract_name?(path)
-
/linux/ =~ RUBY_PLATFORM && /\A(\0|\z)/ =~ path
-
end
-
end
-
-
# creates a UNIX socket server on _path_.
-
# It calls the block for each socket accepted.
-
#
-
# If _host_ is specified, it is used with _port_ to determine the server ports.
-
#
-
# The socket is *not* closed when the block returns.
-
# So application should close it.
-
#
-
# This method deletes the socket file pointed by _path_ at first if
-
# the file is a socket file and it is owned by the user of the application.
-
# This is safe only if the directory of _path_ is not changed by a malicious user.
-
# So don't use /tmp/malicious-users-directory/socket.
-
# Note that /tmp/socket and /tmp/your-private-directory/socket is safe assuming that /tmp has sticky bit.
-
#
-
# # Sequential echo server.
-
# # It services only one client at a time.
-
# Socket.unix_server_loop("/tmp/sock") {|sock, client_addrinfo|
-
# begin
-
# IO.copy_stream(sock, sock)
-
# ensure
-
# sock.close
-
# end
-
# }
-
#
-
1
def self.unix_server_loop(path, &b) # :yield: socket, client_addrinfo
-
unix_server_socket(path) {|serv|
-
accept_loop(serv, &b)
-
}
-
end
-
-
# call-seq:
-
# socket.connect_nonblock(remote_sockaddr, [options]) => 0
-
#
-
# Requests a connection to be made on the given +remote_sockaddr+ after
-
# O_NONBLOCK is set for the underlying file descriptor.
-
# Returns 0 if successful, otherwise an exception is raised.
-
#
-
# === Parameter
-
# # +remote_sockaddr+ - the +struct+ sockaddr contained in a string or Addrinfo object
-
#
-
# === Example:
-
# # Pull down Google's web page
-
# require 'socket'
-
# include Socket::Constants
-
# socket = Socket.new(AF_INET, SOCK_STREAM, 0)
-
# sockaddr = Socket.sockaddr_in(80, 'www.google.com')
-
# begin # emulate blocking connect
-
# socket.connect_nonblock(sockaddr)
-
# rescue IO::WaitWritable
-
# IO.select(nil, [socket]) # wait 3-way handshake completion
-
# begin
-
# socket.connect_nonblock(sockaddr) # check connection failure
-
# rescue Errno::EISCONN
-
# end
-
# end
-
# socket.write("GET / HTTP/1.0\r\n\r\n")
-
# results = socket.read
-
#
-
# Refer to Socket#connect for the exceptions that may be thrown if the call
-
# to _connect_nonblock_ fails.
-
#
-
# Socket#connect_nonblock may raise any error corresponding to connect(2) failure,
-
# including Errno::EINPROGRESS.
-
#
-
# If the exception is Errno::EINPROGRESS,
-
# it is extended by IO::WaitWritable.
-
# So IO::WaitWritable can be used to rescue the exceptions for retrying connect_nonblock.
-
#
-
# By specifying a keyword argument _exception_ to +false+, you can indicate
-
# that connect_nonblock should not raise an IO::WaitWritable exception, but
-
# return the symbol +:wait_writable+ instead.
-
#
-
# === See
-
# # Socket#connect
-
1
def connect_nonblock(addr, exception: true)
-
__connect_nonblock(addr, exception)
-
end
-
end
-
-
1
class UDPSocket < IPSocket
-
-
# call-seq:
-
# udpsocket.recvfrom_nonblock(maxlen [, flags[, outbuf [, options]]]) => [mesg, sender_inet_addr]
-
#
-
# Receives up to _maxlen_ bytes from +udpsocket+ using recvfrom(2) after
-
# O_NONBLOCK is set for the underlying file descriptor.
-
# _flags_ is zero or more of the +MSG_+ options.
-
# The first element of the results, _mesg_, is the data received.
-
# The second element, _sender_inet_addr_, is an array to represent the sender address.
-
#
-
# When recvfrom(2) returns 0,
-
# Socket#recvfrom_nonblock returns an empty string as data.
-
# It means an empty packet.
-
#
-
# === Parameters
-
# * +maxlen+ - the number of bytes to receive from the socket
-
# * +flags+ - zero or more of the +MSG_+ options
-
# * +outbuf+ - destination String buffer
-
# * +options+ - keyword hash, supporting `exception: false`
-
#
-
# === Example
-
# require 'socket'
-
# s1 = UDPSocket.new
-
# s1.bind("127.0.0.1", 0)
-
# s2 = UDPSocket.new
-
# s2.bind("127.0.0.1", 0)
-
# s2.connect(*s1.addr.values_at(3,1))
-
# s1.connect(*s2.addr.values_at(3,1))
-
# s1.send "aaa", 0
-
# begin # emulate blocking recvfrom
-
# p s2.recvfrom_nonblock(10) #=> ["aaa", ["AF_INET", 33302, "localhost.localdomain", "127.0.0.1"]]
-
# rescue IO::WaitReadable
-
# IO.select([s2])
-
# retry
-
# end
-
#
-
# Refer to Socket#recvfrom for the exceptions that may be thrown if the call
-
# to _recvfrom_nonblock_ fails.
-
#
-
# UDPSocket#recvfrom_nonblock may raise any error corresponding to recvfrom(2) failure,
-
# including Errno::EWOULDBLOCK.
-
#
-
# If the exception is Errno::EWOULDBLOCK or Errno::EAGAIN,
-
# it is extended by IO::WaitReadable.
-
# So IO::WaitReadable can be used to rescue the exceptions for retrying recvfrom_nonblock.
-
#
-
# By specifying a keyword argument _exception_ to +false+, you can indicate
-
# that recvfrom_nonblock should not raise an IO::WaitReadable exception, but
-
# return the symbol +:wait_readable+ instead.
-
#
-
# === See
-
# * Socket#recvfrom
-
1
def recvfrom_nonblock(len, flag = 0, outbuf = nil, exception: true)
-
__recvfrom_nonblock(len, flag, outbuf, exception)
-
end
-
end
-
-
1
class TCPServer < TCPSocket
-
-
# call-seq:
-
# tcpserver.accept_nonblock([options]) => tcpsocket
-
#
-
# Accepts an incoming connection using accept(2) after
-
# O_NONBLOCK is set for the underlying file descriptor.
-
# It returns an accepted TCPSocket for the incoming connection.
-
#
-
# === Example
-
# require 'socket'
-
# serv = TCPServer.new(2202)
-
# begin # emulate blocking accept
-
# sock = serv.accept_nonblock
-
# rescue IO::WaitReadable, Errno::EINTR
-
# IO.select([serv])
-
# retry
-
# end
-
# # sock is an accepted socket.
-
#
-
# Refer to Socket#accept for the exceptions that may be thrown if the call
-
# to TCPServer#accept_nonblock fails.
-
#
-
# TCPServer#accept_nonblock may raise any error corresponding to accept(2) failure,
-
# including Errno::EWOULDBLOCK.
-
#
-
# If the exception is Errno::EWOULDBLOCK, Errno::EAGAIN, Errno::ECONNABORTED, Errno::EPROTO,
-
# it is extended by IO::WaitReadable.
-
# So IO::WaitReadable can be used to rescue the exceptions for retrying accept_nonblock.
-
#
-
# By specifying a keyword argument _exception_ to +false+, you can indicate
-
# that accept_nonblock should not raise an IO::WaitReadable exception, but
-
# return the symbol +:wait_readable+ instead.
-
#
-
# === See
-
# * TCPServer#accept
-
# * Socket#accept
-
1
def accept_nonblock(exception: true)
-
__accept_nonblock(exception)
-
end
-
end
-
-
class UNIXServer < UNIXSocket
-
# call-seq:
-
# unixserver.accept_nonblock([options]) => unixsocket
-
#
-
# Accepts an incoming connection using accept(2) after
-
# O_NONBLOCK is set for the underlying file descriptor.
-
# It returns an accepted UNIXSocket for the incoming connection.
-
#
-
# === Example
-
# require 'socket'
-
# serv = UNIXServer.new("/tmp/sock")
-
# begin # emulate blocking accept
-
# sock = serv.accept_nonblock
-
# rescue IO::WaitReadable, Errno::EINTR
-
# IO.select([serv])
-
# retry
-
# end
-
# # sock is an accepted socket.
-
#
-
# Refer to Socket#accept for the exceptions that may be thrown if the call
-
# to UNIXServer#accept_nonblock fails.
-
#
-
# UNIXServer#accept_nonblock may raise any error corresponding to accept(2) failure,
-
# including Errno::EWOULDBLOCK.
-
#
-
# If the exception is Errno::EWOULDBLOCK, Errno::EAGAIN, Errno::ECONNABORTED or Errno::EPROTO,
-
# it is extended by IO::WaitReadable.
-
# So IO::WaitReadable can be used to rescue the exceptions for retrying accept_nonblock.
-
#
-
# By specifying a keyword argument _exception_ to +false+, you can indicate
-
# that accept_nonblock should not raise an IO::WaitReadable exception, but
-
# return the symbol +:wait_readable+ instead.
-
#
-
# === See
-
# * UNIXServer#accept
-
# * Socket#accept
-
1
def accept_nonblock(exception: true)
-
__accept_nonblock(exception)
-
end
-
1
end if defined?(UNIXSocket)
-
# frozen_string_literal: true
-
#
-
# tempfile - manipulates temporary files
-
#
-
# $Id$
-
#
-
-
1
require 'delegate'
-
1
require 'tmpdir'
-
-
# A utility class for managing temporary files. When you create a Tempfile
-
# object, it will create a temporary file with a unique filename. A Tempfile
-
# objects behaves just like a File object, and you can perform all the usual
-
# file operations on it: reading data, writing data, changing its permissions,
-
# etc. So although this class does not explicitly document all instance methods
-
# supported by File, you can in fact call any File instance method on a
-
# Tempfile object.
-
#
-
# == Synopsis
-
#
-
# require 'tempfile'
-
#
-
# file = Tempfile.new('foo')
-
# file.path # => A unique filename in the OS's temp directory,
-
# # e.g.: "/tmp/foo.24722.0"
-
# # This filename contains 'foo' in its basename.
-
# file.write("hello world")
-
# file.rewind
-
# file.read # => "hello world"
-
# file.close
-
# file.unlink # deletes the temp file
-
#
-
# == Good practices
-
#
-
# === Explicit close
-
#
-
# When a Tempfile object is garbage collected, or when the Ruby interpreter
-
# exits, its associated temporary file is automatically deleted. This means
-
# that's it's unnecessary to explicitly delete a Tempfile after use, though
-
# it's good practice to do so: not explicitly deleting unused Tempfiles can
-
# potentially leave behind large amounts of tempfiles on the filesystem
-
# until they're garbage collected. The existence of these temp files can make
-
# it harder to determine a new Tempfile filename.
-
#
-
# Therefore, one should always call #unlink or close in an ensure block, like
-
# this:
-
#
-
# file = Tempfile.new('foo')
-
# begin
-
# # ...do something with file...
-
# ensure
-
# file.close
-
# file.unlink # deletes the temp file
-
# end
-
#
-
# === Unlink after creation
-
#
-
# On POSIX systems, it's possible to unlink a file right after creating it,
-
# and before closing it. This removes the filesystem entry without closing
-
# the file handle, so it ensures that only the processes that already had
-
# the file handle open can access the file's contents. It's strongly
-
# recommended that you do this if you do not want any other processes to
-
# be able to read from or write to the Tempfile, and you do not need to
-
# know the Tempfile's filename either.
-
#
-
# For example, a practical use case for unlink-after-creation would be this:
-
# you need a large byte buffer that's too large to comfortably fit in RAM,
-
# e.g. when you're writing a web server and you want to buffer the client's
-
# file upload data.
-
#
-
# Please refer to #unlink for more information and a code example.
-
#
-
# == Minor notes
-
#
-
# Tempfile's filename picking method is both thread-safe and inter-process-safe:
-
# it guarantees that no other threads or processes will pick the same filename.
-
#
-
# Tempfile itself however may not be entirely thread-safe. If you access the
-
# same Tempfile object from multiple threads then you should protect it with a
-
# mutex.
-
1
class Tempfile < DelegateClass(File)
-
# Creates a temporary file with permissions 0600 (= only readable and
-
# writable by the owner) and opens it with mode "w+".
-
#
-
# The +basename+ parameter is used to determine the name of the
-
# temporary file. You can either pass a String or an Array with
-
# 2 String elements. In the former form, the temporary file's base
-
# name will begin with the given string. In the latter form,
-
# the temporary file's base name will begin with the array's first
-
# element, and end with the second element. For example:
-
#
-
# file = Tempfile.new('hello')
-
# file.path # => something like: "/tmp/hello2843-8392-92849382--0"
-
#
-
# # Use the Array form to enforce an extension in the filename:
-
# file = Tempfile.new(['hello', '.jpg'])
-
# file.path # => something like: "/tmp/hello2843-8392-92849382--0.jpg"
-
#
-
# The temporary file will be placed in the directory as specified
-
# by the +tmpdir+ parameter. By default, this is +Dir.tmpdir+.
-
#
-
# file = Tempfile.new('hello', '/home/aisaka')
-
# file.path # => something like: "/home/aisaka/hello2843-8392-92849382--0"
-
#
-
# You can also pass an options hash. Under the hood, Tempfile creates
-
# the temporary file using +File.open+. These options will be passed to
-
# +File.open+. This is mostly useful for specifying encoding
-
# options, e.g.:
-
#
-
# Tempfile.new('hello', '/home/aisaka', encoding: 'ascii-8bit')
-
#
-
# # You can also omit the 'tmpdir' parameter:
-
# Tempfile.new('hello', encoding: 'ascii-8bit')
-
#
-
# Note: +mode+ keyword argument, as accepted by Tempfile, can only be
-
# numeric, combination of the modes defined in File::Constants.
-
#
-
# === Exceptions
-
#
-
# If Tempfile.new cannot find a unique filename within a limited
-
# number of tries, then it will raise an exception.
-
1
def initialize(basename="", tmpdir=nil, mode: 0, **options)
-
warn "Tempfile.new doesn't call the given block.", uplevel: 1 if block_given?
-
-
@unlinked = false
-
@mode = mode|File::RDWR|File::CREAT|File::EXCL
-
::Dir::Tmpname.create(basename, tmpdir, **options) do |tmpname, n, opts|
-
opts[:perm] = 0600
-
@tmpfile = File.open(tmpname, @mode, **opts)
-
@opts = opts.freeze
-
end
-
ObjectSpace.define_finalizer(self, Remover.new(@tmpfile))
-
-
super(@tmpfile)
-
end
-
-
# Opens or reopens the file with mode "r+".
-
1
def open
-
_close
-
mode = @mode & ~(File::CREAT|File::EXCL)
-
@tmpfile = File.open(@tmpfile.path, mode, **@opts)
-
__setobj__(@tmpfile)
-
end
-
-
1
def _close # :nodoc:
-
@tmpfile.close
-
end
-
1
protected :_close
-
-
# Closes the file. If +unlink_now+ is true, then the file will be unlinked
-
# (deleted) after closing. Of course, you can choose to later call #unlink
-
# if you do not unlink it now.
-
#
-
# If you don't explicitly unlink the temporary file, the removal
-
# will be delayed until the object is finalized.
-
1
def close(unlink_now=false)
-
_close
-
unlink if unlink_now
-
end
-
-
# Closes and unlinks (deletes) the file. Has the same effect as called
-
# <tt>close(true)</tt>.
-
1
def close!
-
close(true)
-
end
-
-
# Unlinks (deletes) the file from the filesystem. One should always unlink
-
# the file after using it, as is explained in the "Explicit close" good
-
# practice section in the Tempfile overview:
-
#
-
# file = Tempfile.new('foo')
-
# begin
-
# # ...do something with file...
-
# ensure
-
# file.close
-
# file.unlink # deletes the temp file
-
# end
-
#
-
# === Unlink-before-close
-
#
-
# On POSIX systems it's possible to unlink a file before closing it. This
-
# practice is explained in detail in the Tempfile overview (section
-
# "Unlink after creation"); please refer there for more information.
-
#
-
# However, unlink-before-close may not be supported on non-POSIX operating
-
# systems. Microsoft Windows is the most notable case: unlinking a non-closed
-
# file will result in an error, which this method will silently ignore. If
-
# you want to practice unlink-before-close whenever possible, then you should
-
# write code like this:
-
#
-
# file = Tempfile.new('foo')
-
# file.unlink # On Windows this silently fails.
-
# begin
-
# # ... do something with file ...
-
# ensure
-
# file.close! # Closes the file handle. If the file wasn't unlinked
-
# # because #unlink failed, then this method will attempt
-
# # to do so again.
-
# end
-
1
def unlink
-
return if @unlinked
-
begin
-
File.unlink(@tmpfile.path)
-
rescue Errno::ENOENT
-
rescue Errno::EACCES
-
# may not be able to unlink on Windows; just ignore
-
return
-
end
-
ObjectSpace.undefine_finalizer(self)
-
@unlinked = true
-
end
-
1
alias delete unlink
-
-
# Returns the full path name of the temporary file.
-
# This will be nil if #unlink has been called.
-
1
def path
-
@unlinked ? nil : @tmpfile.path
-
end
-
-
# Returns the size of the temporary file. As a side effect, the IO
-
# buffer is flushed before determining the size.
-
1
def size
-
if !@tmpfile.closed?
-
@tmpfile.size # File#size calls rb_io_flush_raw()
-
else
-
File.size(@tmpfile.path)
-
end
-
end
-
1
alias length size
-
-
# :stopdoc:
-
1
def inspect
-
if @tmpfile.closed?
-
"#<#{self.class}:#{path} (closed)>"
-
else
-
"#<#{self.class}:#{path}>"
-
end
-
end
-
-
1
class Remover # :nodoc:
-
1
def initialize(tmpfile)
-
@pid = Process.pid
-
@tmpfile = tmpfile
-
end
-
-
1
def call(*args)
-
return if @pid != Process.pid
-
-
$stderr.puts "removing #{@tmpfile.path}..." if $DEBUG
-
-
@tmpfile.close
-
begin
-
File.unlink(@tmpfile.path)
-
rescue Errno::ENOENT
-
end
-
-
$stderr.puts "done" if $DEBUG
-
end
-
end
-
-
1
class << self
-
# :startdoc:
-
-
# Creates a new Tempfile.
-
#
-
# If no block is given, this is a synonym for Tempfile.new.
-
#
-
# If a block is given, then a Tempfile object will be constructed,
-
# and the block is run with said object as argument. The Tempfile
-
# object will be automatically closed after the block terminates.
-
# The call returns the value of the block.
-
#
-
# In any case, all arguments (<code>*args</code>) will be passed to Tempfile.new.
-
#
-
# Tempfile.open('foo', '/home/temp') do |f|
-
# # ... do something with f ...
-
# end
-
#
-
# # Equivalent:
-
# f = Tempfile.open('foo', '/home/temp')
-
# begin
-
# # ... do something with f ...
-
# ensure
-
# f.close
-
# end
-
1
def open(*args, **kw)
-
tempfile = new(*args, **kw)
-
-
if block_given?
-
begin
-
yield(tempfile)
-
ensure
-
tempfile.close
-
end
-
else
-
tempfile
-
end
-
end
-
end
-
end
-
-
# Creates a temporary file as usual File object (not Tempfile).
-
# It doesn't use finalizer and delegation.
-
#
-
# If no block is given, this is similar to Tempfile.new except
-
# creating File instead of Tempfile.
-
# The created file is not removed automatically.
-
# You should use File.unlink to remove it.
-
#
-
# If a block is given, then a File object will be constructed,
-
# and the block is invoked with the object as the argument.
-
# The File object will be automatically closed and
-
# the temporary file is removed after the block terminates.
-
# The call returns the value of the block.
-
#
-
# In any case, all arguments (+basename+, +tmpdir+, +mode+, and
-
# <code>**options</code>) will be treated as Tempfile.new.
-
#
-
# Tempfile.create('foo', '/home/temp') do |f|
-
# # ... do something with f ...
-
# end
-
#
-
1
def Tempfile.create(basename="", tmpdir=nil, mode: 0, **options)
-
tmpfile = nil
-
Dir::Tmpname.create(basename, tmpdir, **options) do |tmpname, n, opts|
-
mode |= File::RDWR|File::CREAT|File::EXCL
-
opts[:perm] = 0600
-
tmpfile = File.open(tmpname, mode, **opts)
-
end
-
if block_given?
-
begin
-
yield tmpfile
-
ensure
-
unless tmpfile.closed?
-
if File.identical?(tmpfile, tmpfile.path)
-
unlinked = File.unlink tmpfile.path rescue nil
-
end
-
tmpfile.close
-
end
-
unless unlinked
-
begin
-
File.unlink tmpfile.path
-
rescue Errno::ENOENT
-
end
-
end
-
end
-
else
-
tmpfile
-
end
-
end
-
# frozen_string_literal: false
-
# Timeout long-running blocks
-
#
-
# == Synopsis
-
#
-
# require 'timeout'
-
# status = Timeout::timeout(5) {
-
# # Something that should be interrupted if it takes more than 5 seconds...
-
# }
-
#
-
# == Description
-
#
-
# Timeout provides a way to auto-terminate a potentially long-running
-
# operation if it hasn't finished in a fixed amount of time.
-
#
-
# Previous versions didn't use a module for namespacing, however
-
# #timeout is provided for backwards compatibility. You
-
# should prefer Timeout.timeout instead.
-
#
-
# == Copyright
-
#
-
# Copyright:: (C) 2000 Network Applied Communication Laboratory, Inc.
-
# Copyright:: (C) 2000 Information-technology Promotion Agency, Japan
-
-
1
module Timeout
-
# Raised by Timeout.timeout when the block times out.
-
1
class Error < RuntimeError
-
1
attr_reader :thread
-
-
1
def self.catch(*args)
-
exc = new(*args)
-
exc.instance_variable_set(:@thread, Thread.current)
-
::Kernel.catch(exc) {yield exc}
-
end
-
-
1
def exception(*)
-
# TODO: use Fiber.current to see if self can be thrown
-
if self.thread == Thread.current
-
bt = caller
-
begin
-
throw(self, bt)
-
rescue UncaughtThrowError
-
end
-
end
-
self
-
end
-
end
-
-
# :stopdoc:
-
1
THIS_FILE = /\A#{Regexp.quote(__FILE__)}:/o
-
1
CALLER_OFFSET = ((c = caller[0]) && THIS_FILE =~ c) ? 1 : 0
-
1
private_constant :THIS_FILE, :CALLER_OFFSET
-
# :startdoc:
-
-
# Perform an operation in a block, raising an error if it takes longer than
-
# +sec+ seconds to complete.
-
#
-
# +sec+:: Number of seconds to wait for the block to terminate. Any number
-
# may be used, including Floats to specify fractional seconds. A
-
# value of 0 or +nil+ will execute the block without any timeout.
-
# +klass+:: Exception Class to raise if the block fails to terminate
-
# in +sec+ seconds. Omitting will use the default, Timeout::Error
-
# +message+:: Error message to raise with Exception Class.
-
# Omitting will use the default, "execution expired"
-
#
-
# Returns the result of the block *if* the block completed before
-
# +sec+ seconds, otherwise throws an exception, based on the value of +klass+.
-
#
-
# The exception thrown to terminate the given block cannot be rescued inside
-
# the block unless +klass+ is given explicitly. However, the block can use
-
# ensure to prevent the handling of the exception. For that reason, this
-
# method cannot be relied on to enforce timeouts for untrusted blocks.
-
#
-
# Note that this is both a method of module Timeout, so you can <tt>include
-
# Timeout</tt> into your classes so they have a #timeout method, as well as
-
# a module method, so you can call it directly as Timeout.timeout().
-
1
def timeout(sec, klass = nil, message = nil) #:yield: +sec+
-
3
return yield(sec) if sec == nil or sec.zero?
-
3
message ||= "execution expired".freeze
-
3
from = "from #{caller_locations(1, 1)[0]}" if $DEBUG
-
3
e = Error
-
3
bl = proc do |exception|
-
begin
-
3
x = Thread.current
-
3
y = Thread.start {
-
3
Thread.current.name = from
-
begin
-
3
sleep sec
-
rescue => e
-
x.raise e
-
else
-
x.raise exception, message
-
end
-
}
-
3
return yield(sec)
-
ensure
-
3
if y
-
3
y.kill
-
3
y.join # make sure y is dead.
-
end
-
end
-
end
-
3
if klass
-
begin
-
3
bl.call(klass)
-
rescue klass => e
-
bt = e.backtrace
-
end
-
else
-
bt = Error.catch(message, &bl)
-
end
-
level = -caller(CALLER_OFFSET).size-2
-
while THIS_FILE =~ bt[level]
-
bt.delete_at(level)
-
end
-
raise(e, message, bt)
-
end
-
-
1
module_function :timeout
-
end
-
-
1
def timeout(*args, &block)
-
warn "Object##{__method__} is deprecated, use Timeout.timeout instead.", uplevel: 1
-
Timeout.timeout(*args, &block)
-
end
-
-
# Another name for Timeout::Error, defined for backwards compatibility with
-
# earlier versions of timeout.rb.
-
1
TimeoutError = Timeout::Error
-
1
class Object
-
1
deprecate_constant :TimeoutError
-
end
-
# frozen_string_literal: true
-
#
-
# tmpdir - retrieve temporary directory path
-
#
-
# $Id$
-
#
-
-
1
require 'fileutils'
-
begin
-
1
require 'etc.so'
-
rescue LoadError # rescue LoadError for miniruby
-
end
-
-
1
class Dir
-
-
1
@@systmpdir ||= defined?(Etc.systmpdir) ? Etc.systmpdir : '/tmp'
-
-
##
-
# Returns the operating system's temporary file path.
-
-
1
def self.tmpdir
-
tmp = nil
-
[ENV['TMPDIR'], ENV['TMP'], ENV['TEMP'], @@systmpdir, '/tmp', '.'].each do |dir|
-
next if !dir
-
dir = File.expand_path(dir)
-
if stat = File.stat(dir) and stat.directory? and stat.writable? and
-
(!stat.world_writable? or stat.sticky?)
-
tmp = dir
-
break
-
end rescue nil
-
end
-
raise ArgumentError, "could not find a temporary directory" unless tmp
-
tmp
-
end
-
-
# Dir.mktmpdir creates a temporary directory.
-
#
-
# The directory is created with 0700 permission.
-
# Application should not change the permission to make the temporary directory accessible from other users.
-
#
-
# The prefix and suffix of the name of the directory is specified by
-
# the optional first argument, <i>prefix_suffix</i>.
-
# - If it is not specified or nil, "d" is used as the prefix and no suffix is used.
-
# - If it is a string, it is used as the prefix and no suffix is used.
-
# - If it is an array, first element is used as the prefix and second element is used as a suffix.
-
#
-
# Dir.mktmpdir {|dir| dir is ".../d..." }
-
# Dir.mktmpdir("foo") {|dir| dir is ".../foo..." }
-
# Dir.mktmpdir(["foo", "bar"]) {|dir| dir is ".../foo...bar" }
-
#
-
# The directory is created under Dir.tmpdir or
-
# the optional second argument <i>tmpdir</i> if non-nil value is given.
-
#
-
# Dir.mktmpdir {|dir| dir is "#{Dir.tmpdir}/d..." }
-
# Dir.mktmpdir(nil, "/var/tmp") {|dir| dir is "/var/tmp/d..." }
-
#
-
# If a block is given,
-
# it is yielded with the path of the directory.
-
# The directory and its contents are removed
-
# using FileUtils.remove_entry before Dir.mktmpdir returns.
-
# The value of the block is returned.
-
#
-
# Dir.mktmpdir {|dir|
-
# # use the directory...
-
# open("#{dir}/foo", "w") { ... }
-
# }
-
#
-
# If a block is not given,
-
# The path of the directory is returned.
-
# In this case, Dir.mktmpdir doesn't remove the directory.
-
#
-
# dir = Dir.mktmpdir
-
# begin
-
# # use the directory...
-
# open("#{dir}/foo", "w") { ... }
-
# ensure
-
# # remove the directory.
-
# FileUtils.remove_entry dir
-
# end
-
#
-
1
def self.mktmpdir(prefix_suffix=nil, *rest, **options)
-
base = nil
-
path = Tmpname.create(prefix_suffix || "d", *rest, **options) {|path, _, _, d|
-
base = d
-
mkdir(path, 0700)
-
}
-
if block_given?
-
begin
-
yield path
-
ensure
-
unless base
-
stat = File.stat(File.dirname(path))
-
if stat.world_writable? and !stat.sticky?
-
raise ArgumentError, "parent directory is world writable but not sticky"
-
end
-
end
-
FileUtils.remove_entry path
-
end
-
else
-
path
-
end
-
end
-
-
1
module Tmpname # :nodoc:
-
1
module_function
-
-
1
def tmpdir
-
Dir.tmpdir
-
end
-
-
1
UNUSABLE_CHARS = [File::SEPARATOR, File::ALT_SEPARATOR, File::PATH_SEPARATOR, ":"].uniq.join("").freeze
-
-
1
def create(basename, tmpdir=nil, max_try: nil, **opts)
-
origdir = tmpdir
-
tmpdir ||= tmpdir()
-
n = nil
-
prefix, suffix = basename
-
prefix = (String.try_convert(prefix) or
-
raise ArgumentError, "unexpected prefix: #{prefix.inspect}")
-
prefix = prefix.delete(UNUSABLE_CHARS)
-
suffix &&= (String.try_convert(suffix) or
-
raise ArgumentError, "unexpected suffix: #{suffix.inspect}")
-
suffix &&= suffix.delete(UNUSABLE_CHARS)
-
begin
-
t = Time.now.strftime("%Y%m%d")
-
path = "#{prefix}#{t}-#{$$}-#{rand(0x100000000).to_s(36)}"\
-
"#{n ? %[-#{n}] : ''}#{suffix||''}"
-
path = File.join(tmpdir, path)
-
yield(path, n, opts, origdir)
-
rescue Errno::EEXIST
-
n ||= 0
-
n += 1
-
retry if !max_try or n < max_try
-
raise "cannot generate temporary name using `#{basename}' under `#{tmpdir}'"
-
end
-
path
-
end
-
end
-
end
-
# frozen_string_literal: true
-
#--
-
# = uri/common.rb
-
#
-
# Author:: Akira Yamada <akira@ruby-lang.org>
-
# Revision:: $Id$
-
# License::
-
# You can redistribute it and/or modify it under the same term as Ruby.
-
#
-
# See URI for general documentation
-
#
-
-
1
require_relative "rfc2396_parser"
-
1
require_relative "rfc3986_parser"
-
-
1
module URI
-
1
REGEXP = RFC2396_REGEXP
-
1
Parser = RFC2396_Parser
-
1
RFC3986_PARSER = RFC3986_Parser.new
-
-
# URI::Parser.new
-
1
DEFAULT_PARSER = Parser.new
-
1
DEFAULT_PARSER.pattern.each_pair do |sym, str|
-
31
unless REGEXP::PATTERN.const_defined?(sym)
-
25
REGEXP::PATTERN.const_set(sym, str)
-
end
-
end
-
1
DEFAULT_PARSER.regexp.each_pair do |sym, str|
-
17
const_set(sym, str)
-
end
-
-
1
module Util # :nodoc:
-
1
def make_components_hash(klass, array_hash)
-
tmp = {}
-
if array_hash.kind_of?(Array) &&
-
array_hash.size == klass.component.size - 1
-
klass.component[1..-1].each_index do |i|
-
begin
-
tmp[klass.component[i + 1]] = array_hash[i].clone
-
rescue TypeError
-
tmp[klass.component[i + 1]] = array_hash[i]
-
end
-
end
-
-
elsif array_hash.kind_of?(Hash)
-
array_hash.each do |key, value|
-
begin
-
tmp[key] = value.clone
-
rescue TypeError
-
tmp[key] = value
-
end
-
end
-
else
-
raise ArgumentError,
-
"expected Array of or Hash of components of #{klass} (#{klass.component[1..-1].join(', ')})"
-
end
-
tmp[:scheme] = klass.to_s.sub(/\A.*::/, '').downcase
-
-
return tmp
-
end
-
1
module_function :make_components_hash
-
end
-
-
# Module for escaping unsafe characters with codes.
-
1
module Escape
-
#
-
# == Synopsis
-
#
-
# URI.escape(str [, unsafe])
-
#
-
# == Args
-
#
-
# +str+::
-
# String to replaces in.
-
# +unsafe+::
-
# Regexp that matches all symbols that must be replaced with codes.
-
# By default uses <tt>UNSAFE</tt>.
-
# When this argument is a String, it represents a character set.
-
#
-
# == Description
-
#
-
# Escapes the string, replacing all unsafe characters with codes.
-
#
-
# This method is obsolete and should not be used. Instead, use
-
# CGI.escape, URI.encode_www_form or URI.encode_www_form_component
-
# depending on your specific use case.
-
#
-
# == Usage
-
#
-
# require 'uri'
-
#
-
# enc_uri = URI.escape("http://example.com/?a=\11\15")
-
# # => "http://example.com/?a=%09%0D"
-
#
-
# URI.unescape(enc_uri)
-
# # => "http://example.com/?a=\t\r"
-
#
-
# URI.escape("@?@!", "!?")
-
# # => "@%3F@%21"
-
#
-
1
def escape(*arg)
-
warn "URI.escape is obsolete", uplevel: 1
-
DEFAULT_PARSER.escape(*arg)
-
end
-
1
alias encode escape
-
#
-
# == Synopsis
-
#
-
# URI.unescape(str)
-
#
-
# == Args
-
#
-
# +str+::
-
# String to unescape.
-
#
-
# == Description
-
#
-
# This method is obsolete and should not be used. Instead, use
-
# CGI.unescape, URI.decode_www_form or URI.decode_www_form_component
-
# depending on your specific use case.
-
#
-
# == Usage
-
#
-
# require 'uri'
-
#
-
# enc_uri = URI.escape("http://example.com/?a=\11\15")
-
# # => "http://example.com/?a=%09%0D"
-
#
-
# URI.unescape(enc_uri)
-
# # => "http://example.com/?a=\t\r"
-
#
-
1
def unescape(*arg)
-
warn "URI.unescape is obsolete", uplevel: 1
-
DEFAULT_PARSER.unescape(*arg)
-
end
-
1
alias decode unescape
-
end # module Escape
-
-
1
extend Escape
-
1
include REGEXP
-
-
1
@@schemes = {}
-
# Returns a Hash of the defined schemes.
-
1
def self.scheme_list
-
3
@@schemes
-
end
-
-
#
-
# Base class for all URI exceptions.
-
#
-
1
class Error < StandardError; end
-
#
-
# Not a URI.
-
#
-
1
class InvalidURIError < Error; end
-
#
-
# Not a URI component.
-
#
-
1
class InvalidComponentError < Error; end
-
#
-
# URI is valid, bad usage is not.
-
#
-
1
class BadURIError < Error; end
-
-
#
-
# == Synopsis
-
#
-
# URI::split(uri)
-
#
-
# == Args
-
#
-
# +uri+::
-
# String with URI.
-
#
-
# == Description
-
#
-
# Splits the string on following parts and returns array with result:
-
#
-
# * Scheme
-
# * Userinfo
-
# * Host
-
# * Port
-
# * Registry
-
# * Path
-
# * Opaque
-
# * Query
-
# * Fragment
-
#
-
# == Usage
-
#
-
# require 'uri'
-
#
-
# URI.split("http://www.ruby-lang.org/")
-
# # => ["http", nil, "www.ruby-lang.org", nil, nil, "/", nil, nil, nil]
-
#
-
1
def self.split(uri)
-
RFC3986_PARSER.split(uri)
-
end
-
-
#
-
# == Synopsis
-
#
-
# URI::parse(uri_str)
-
#
-
# == Args
-
#
-
# +uri_str+::
-
# String with URI.
-
#
-
# == Description
-
#
-
# Creates one of the URI's subclasses instance from the string.
-
#
-
# == Raises
-
#
-
# URI::InvalidURIError::
-
# Raised if URI given is not a correct one.
-
#
-
# == Usage
-
#
-
# require 'uri'
-
#
-
# uri = URI.parse("http://www.ruby-lang.org/")
-
# # => #<URI::HTTP http://www.ruby-lang.org/>
-
# uri.scheme
-
# # => "http"
-
# uri.host
-
# # => "www.ruby-lang.org"
-
#
-
# It's recommended to first ::escape the provided +uri_str+ if there are any
-
# invalid URI characters.
-
#
-
1
def self.parse(uri)
-
3
RFC3986_PARSER.parse(uri)
-
end
-
-
#
-
# == Synopsis
-
#
-
# URI::join(str[, str, ...])
-
#
-
# == Args
-
#
-
# +str+::
-
# String(s) to work with, will be converted to RFC3986 URIs before merging.
-
#
-
# == Description
-
#
-
# Joins URIs.
-
#
-
# == Usage
-
#
-
# require 'uri'
-
#
-
# URI.join("http://example.com/","main.rbx")
-
# # => #<URI::HTTP http://example.com/main.rbx>
-
#
-
# URI.join('http://example.com', 'foo')
-
# # => #<URI::HTTP http://example.com/foo>
-
#
-
# URI.join('http://example.com', '/foo', '/bar')
-
# # => #<URI::HTTP http://example.com/bar>
-
#
-
# URI.join('http://example.com', '/foo', 'bar')
-
# # => #<URI::HTTP http://example.com/bar>
-
#
-
# URI.join('http://example.com', '/foo/', 'bar')
-
# # => #<URI::HTTP http://example.com/foo/bar>
-
#
-
1
def self.join(*str)
-
RFC3986_PARSER.join(*str)
-
end
-
-
#
-
# == Synopsis
-
#
-
# URI::extract(str[, schemes][,&blk])
-
#
-
# == Args
-
#
-
# +str+::
-
# String to extract URIs from.
-
# +schemes+::
-
# Limit URI matching to specific schemes.
-
#
-
# == Description
-
#
-
# Extracts URIs from a string. If block given, iterates through all matched URIs.
-
# Returns nil if block given or array with matches.
-
#
-
# == Usage
-
#
-
# require "uri"
-
#
-
# URI.extract("text here http://foo.example.org/bla and here mailto:test@example.com and here also.")
-
# # => ["http://foo.example.com/bla", "mailto:test@example.com"]
-
#
-
1
def self.extract(str, schemes = nil, &block)
-
warn "URI.extract is obsolete", uplevel: 1 if $VERBOSE
-
DEFAULT_PARSER.extract(str, schemes, &block)
-
end
-
-
#
-
# == Synopsis
-
#
-
# URI::regexp([match_schemes])
-
#
-
# == Args
-
#
-
# +match_schemes+::
-
# Array of schemes. If given, resulting regexp matches to URIs
-
# whose scheme is one of the match_schemes.
-
#
-
# == Description
-
#
-
# Returns a Regexp object which matches to URI-like strings.
-
# The Regexp object returned by this method includes arbitrary
-
# number of capture group (parentheses). Never rely on it's number.
-
#
-
# == Usage
-
#
-
# require 'uri'
-
#
-
# # extract first URI from html_string
-
# html_string.slice(URI.regexp)
-
#
-
# # remove ftp URIs
-
# html_string.sub(URI.regexp(['ftp']), '')
-
#
-
# # You should not rely on the number of parentheses
-
# html_string.scan(URI.regexp) do |*matches|
-
# p $&
-
# end
-
#
-
1
def self.regexp(schemes = nil)
-
warn "URI.regexp is obsolete", uplevel: 1 if $VERBOSE
-
DEFAULT_PARSER.make_regexp(schemes)
-
end
-
-
1
TBLENCWWWCOMP_ = {} # :nodoc:
-
1
256.times do |i|
-
256
TBLENCWWWCOMP_[-i.chr] = -('%%%02X' % i)
-
end
-
1
TBLENCWWWCOMP_[' '] = '+'
-
1
TBLENCWWWCOMP_.freeze
-
1
TBLDECWWWCOMP_ = {} # :nodoc:
-
1
256.times do |i|
-
256
h, l = i>>4, i&15
-
256
TBLDECWWWCOMP_[-('%%%X%X' % [h, l])] = -i.chr
-
256
TBLDECWWWCOMP_[-('%%%x%X' % [h, l])] = -i.chr
-
256
TBLDECWWWCOMP_[-('%%%X%x' % [h, l])] = -i.chr
-
256
TBLDECWWWCOMP_[-('%%%x%x' % [h, l])] = -i.chr
-
end
-
1
TBLDECWWWCOMP_['+'] = ' '
-
1
TBLDECWWWCOMP_.freeze
-
-
# Encodes given +str+ to URL-encoded form data.
-
#
-
# This method doesn't convert *, -, ., 0-9, A-Z, _, a-z, but does convert SP
-
# (ASCII space) to + and converts others to %XX.
-
#
-
# If +enc+ is given, convert +str+ to the encoding before percent encoding.
-
#
-
# This is an implementation of
-
# http://www.w3.org/TR/2013/CR-html5-20130806/forms.html#url-encoded-form-data.
-
#
-
# See URI.decode_www_form_component, URI.encode_www_form.
-
1
def self.encode_www_form_component(str, enc=nil)
-
str = str.to_s.dup
-
if str.encoding != Encoding::ASCII_8BIT
-
if enc && enc != Encoding::ASCII_8BIT
-
str.encode!(Encoding::UTF_8, invalid: :replace, undef: :replace)
-
str.encode!(enc, fallback: ->(x){"&##{x.ord};"})
-
end
-
str.force_encoding(Encoding::ASCII_8BIT)
-
end
-
str.gsub!(/[^*\-.0-9A-Z_a-z]/, TBLENCWWWCOMP_)
-
str.force_encoding(Encoding::US_ASCII)
-
end
-
-
# Decodes given +str+ of URL-encoded form data.
-
#
-
# This decodes + to SP.
-
#
-
# See URI.encode_www_form_component, URI.decode_www_form.
-
1
def self.decode_www_form_component(str, enc=Encoding::UTF_8)
-
raise ArgumentError, "invalid %-encoding (#{str})" if /%(?!\h\h)/ =~ str
-
str.b.gsub(/\+|%\h\h/, TBLDECWWWCOMP_).force_encoding(enc)
-
end
-
-
# Generates URL-encoded form data from given +enum+.
-
#
-
# This generates application/x-www-form-urlencoded data defined in HTML5
-
# from given an Enumerable object.
-
#
-
# This internally uses URI.encode_www_form_component(str).
-
#
-
# This method doesn't convert the encoding of given items, so convert them
-
# before calling this method if you want to send data as other than original
-
# encoding or mixed encoding data. (Strings which are encoded in an HTML5
-
# ASCII incompatible encoding are converted to UTF-8.)
-
#
-
# This method doesn't handle files. When you send a file, use
-
# multipart/form-data.
-
#
-
# This refers http://url.spec.whatwg.org/#concept-urlencoded-serializer
-
#
-
# URI.encode_www_form([["q", "ruby"], ["lang", "en"]])
-
# #=> "q=ruby&lang=en"
-
# URI.encode_www_form("q" => "ruby", "lang" => "en")
-
# #=> "q=ruby&lang=en"
-
# URI.encode_www_form("q" => ["ruby", "perl"], "lang" => "en")
-
# #=> "q=ruby&q=perl&lang=en"
-
# URI.encode_www_form([["q", "ruby"], ["q", "perl"], ["lang", "en"]])
-
# #=> "q=ruby&q=perl&lang=en"
-
#
-
# See URI.encode_www_form_component, URI.decode_www_form.
-
1
def self.encode_www_form(enum, enc=nil)
-
enum.map do |k,v|
-
if v.nil?
-
encode_www_form_component(k, enc)
-
elsif v.respond_to?(:to_ary)
-
v.to_ary.map do |w|
-
str = encode_www_form_component(k, enc)
-
unless w.nil?
-
str << '='
-
str << encode_www_form_component(w, enc)
-
end
-
end.join('&')
-
else
-
str = encode_www_form_component(k, enc)
-
str << '='
-
str << encode_www_form_component(v, enc)
-
end
-
end.join('&')
-
end
-
-
# Decodes URL-encoded form data from given +str+.
-
#
-
# This decodes application/x-www-form-urlencoded data
-
# and returns an array of key-value arrays.
-
#
-
# This refers http://url.spec.whatwg.org/#concept-urlencoded-parser,
-
# so this supports only &-separator, and doesn't support ;-separator.
-
#
-
# ary = URI.decode_www_form("a=1&a=2&b=3")
-
# ary #=> [['a', '1'], ['a', '2'], ['b', '3']]
-
# ary.assoc('a').last #=> '1'
-
# ary.assoc('b').last #=> '3'
-
# ary.rassoc('a').last #=> '2'
-
# Hash[ary] #=> {"a"=>"2", "b"=>"3"}
-
#
-
# See URI.decode_www_form_component, URI.encode_www_form.
-
1
def self.decode_www_form(str, enc=Encoding::UTF_8, separator: '&', use__charset_: false, isindex: false)
-
raise ArgumentError, "the input of #{self.name}.#{__method__} must be ASCII only string" unless str.ascii_only?
-
ary = []
-
return ary if str.empty?
-
enc = Encoding.find(enc)
-
str.b.each_line(separator) do |string|
-
string.chomp!(separator)
-
key, sep, val = string.partition('=')
-
if isindex
-
if sep.empty?
-
val = key
-
key = +''
-
end
-
isindex = false
-
end
-
-
if use__charset_ and key == '_charset_' and e = get_encoding(val)
-
enc = e
-
use__charset_ = false
-
end
-
-
key.gsub!(/\+|%\h\h/, TBLDECWWWCOMP_)
-
if val
-
val.gsub!(/\+|%\h\h/, TBLDECWWWCOMP_)
-
else
-
val = +''
-
end
-
-
ary << [key, val]
-
end
-
ary.each do |k, v|
-
k.force_encoding(enc)
-
k.scrub!
-
v.force_encoding(enc)
-
v.scrub!
-
end
-
ary
-
end
-
-
1
private
-
=begin command for WEB_ENCODINGS_
-
curl https://encoding.spec.whatwg.org/encodings.json|
-
ruby -rjson -e 'H={}
-
h={
-
"shift_jis"=>"Windows-31J",
-
"euc-jp"=>"cp51932",
-
"iso-2022-jp"=>"cp50221",
-
"x-mac-cyrillic"=>"macCyrillic",
-
}
-
JSON($<.read).map{|x|x["encodings"]}.flatten.each{|x|
-
Encoding.find(n=h.fetch(n=x["name"].downcase,n))rescue next
-
x["labels"].each{|y|H[y]=n}
-
}
-
puts "{"
-
H.each{|k,v|puts %[ #{k.dump}=>#{v.dump},]}
-
puts "}"
-
'
-
=end
-
1
WEB_ENCODINGS_ = {
-
"unicode-1-1-utf-8"=>"utf-8",
-
"utf-8"=>"utf-8",
-
"utf8"=>"utf-8",
-
"866"=>"ibm866",
-
"cp866"=>"ibm866",
-
"csibm866"=>"ibm866",
-
"ibm866"=>"ibm866",
-
"csisolatin2"=>"iso-8859-2",
-
"iso-8859-2"=>"iso-8859-2",
-
"iso-ir-101"=>"iso-8859-2",
-
"iso8859-2"=>"iso-8859-2",
-
"iso88592"=>"iso-8859-2",
-
"iso_8859-2"=>"iso-8859-2",
-
"iso_8859-2:1987"=>"iso-8859-2",
-
"l2"=>"iso-8859-2",
-
"latin2"=>"iso-8859-2",
-
"csisolatin3"=>"iso-8859-3",
-
"iso-8859-3"=>"iso-8859-3",
-
"iso-ir-109"=>"iso-8859-3",
-
"iso8859-3"=>"iso-8859-3",
-
"iso88593"=>"iso-8859-3",
-
"iso_8859-3"=>"iso-8859-3",
-
"iso_8859-3:1988"=>"iso-8859-3",
-
"l3"=>"iso-8859-3",
-
"latin3"=>"iso-8859-3",
-
"csisolatin4"=>"iso-8859-4",
-
"iso-8859-4"=>"iso-8859-4",
-
"iso-ir-110"=>"iso-8859-4",
-
"iso8859-4"=>"iso-8859-4",
-
"iso88594"=>"iso-8859-4",
-
"iso_8859-4"=>"iso-8859-4",
-
"iso_8859-4:1988"=>"iso-8859-4",
-
"l4"=>"iso-8859-4",
-
"latin4"=>"iso-8859-4",
-
"csisolatincyrillic"=>"iso-8859-5",
-
"cyrillic"=>"iso-8859-5",
-
"iso-8859-5"=>"iso-8859-5",
-
"iso-ir-144"=>"iso-8859-5",
-
"iso8859-5"=>"iso-8859-5",
-
"iso88595"=>"iso-8859-5",
-
"iso_8859-5"=>"iso-8859-5",
-
"iso_8859-5:1988"=>"iso-8859-5",
-
"arabic"=>"iso-8859-6",
-
"asmo-708"=>"iso-8859-6",
-
"csiso88596e"=>"iso-8859-6",
-
"csiso88596i"=>"iso-8859-6",
-
"csisolatinarabic"=>"iso-8859-6",
-
"ecma-114"=>"iso-8859-6",
-
"iso-8859-6"=>"iso-8859-6",
-
"iso-8859-6-e"=>"iso-8859-6",
-
"iso-8859-6-i"=>"iso-8859-6",
-
"iso-ir-127"=>"iso-8859-6",
-
"iso8859-6"=>"iso-8859-6",
-
"iso88596"=>"iso-8859-6",
-
"iso_8859-6"=>"iso-8859-6",
-
"iso_8859-6:1987"=>"iso-8859-6",
-
"csisolatingreek"=>"iso-8859-7",
-
"ecma-118"=>"iso-8859-7",
-
"elot_928"=>"iso-8859-7",
-
"greek"=>"iso-8859-7",
-
"greek8"=>"iso-8859-7",
-
"iso-8859-7"=>"iso-8859-7",
-
"iso-ir-126"=>"iso-8859-7",
-
"iso8859-7"=>"iso-8859-7",
-
"iso88597"=>"iso-8859-7",
-
"iso_8859-7"=>"iso-8859-7",
-
"iso_8859-7:1987"=>"iso-8859-7",
-
"sun_eu_greek"=>"iso-8859-7",
-
"csiso88598e"=>"iso-8859-8",
-
"csisolatinhebrew"=>"iso-8859-8",
-
"hebrew"=>"iso-8859-8",
-
"iso-8859-8"=>"iso-8859-8",
-
"iso-8859-8-e"=>"iso-8859-8",
-
"iso-ir-138"=>"iso-8859-8",
-
"iso8859-8"=>"iso-8859-8",
-
"iso88598"=>"iso-8859-8",
-
"iso_8859-8"=>"iso-8859-8",
-
"iso_8859-8:1988"=>"iso-8859-8",
-
"visual"=>"iso-8859-8",
-
"csisolatin6"=>"iso-8859-10",
-
"iso-8859-10"=>"iso-8859-10",
-
"iso-ir-157"=>"iso-8859-10",
-
"iso8859-10"=>"iso-8859-10",
-
"iso885910"=>"iso-8859-10",
-
"l6"=>"iso-8859-10",
-
"latin6"=>"iso-8859-10",
-
"iso-8859-13"=>"iso-8859-13",
-
"iso8859-13"=>"iso-8859-13",
-
"iso885913"=>"iso-8859-13",
-
"iso-8859-14"=>"iso-8859-14",
-
"iso8859-14"=>"iso-8859-14",
-
"iso885914"=>"iso-8859-14",
-
"csisolatin9"=>"iso-8859-15",
-
"iso-8859-15"=>"iso-8859-15",
-
"iso8859-15"=>"iso-8859-15",
-
"iso885915"=>"iso-8859-15",
-
"iso_8859-15"=>"iso-8859-15",
-
"l9"=>"iso-8859-15",
-
"iso-8859-16"=>"iso-8859-16",
-
"cskoi8r"=>"koi8-r",
-
"koi"=>"koi8-r",
-
"koi8"=>"koi8-r",
-
"koi8-r"=>"koi8-r",
-
"koi8_r"=>"koi8-r",
-
"koi8-ru"=>"koi8-u",
-
"koi8-u"=>"koi8-u",
-
"dos-874"=>"windows-874",
-
"iso-8859-11"=>"windows-874",
-
"iso8859-11"=>"windows-874",
-
"iso885911"=>"windows-874",
-
"tis-620"=>"windows-874",
-
"windows-874"=>"windows-874",
-
"cp1250"=>"windows-1250",
-
"windows-1250"=>"windows-1250",
-
"x-cp1250"=>"windows-1250",
-
"cp1251"=>"windows-1251",
-
"windows-1251"=>"windows-1251",
-
"x-cp1251"=>"windows-1251",
-
"ansi_x3.4-1968"=>"windows-1252",
-
"ascii"=>"windows-1252",
-
"cp1252"=>"windows-1252",
-
"cp819"=>"windows-1252",
-
"csisolatin1"=>"windows-1252",
-
"ibm819"=>"windows-1252",
-
"iso-8859-1"=>"windows-1252",
-
"iso-ir-100"=>"windows-1252",
-
"iso8859-1"=>"windows-1252",
-
"iso88591"=>"windows-1252",
-
"iso_8859-1"=>"windows-1252",
-
"iso_8859-1:1987"=>"windows-1252",
-
"l1"=>"windows-1252",
-
"latin1"=>"windows-1252",
-
"us-ascii"=>"windows-1252",
-
"windows-1252"=>"windows-1252",
-
"x-cp1252"=>"windows-1252",
-
"cp1253"=>"windows-1253",
-
"windows-1253"=>"windows-1253",
-
"x-cp1253"=>"windows-1253",
-
"cp1254"=>"windows-1254",
-
"csisolatin5"=>"windows-1254",
-
"iso-8859-9"=>"windows-1254",
-
"iso-ir-148"=>"windows-1254",
-
"iso8859-9"=>"windows-1254",
-
"iso88599"=>"windows-1254",
-
"iso_8859-9"=>"windows-1254",
-
"iso_8859-9:1989"=>"windows-1254",
-
"l5"=>"windows-1254",
-
"latin5"=>"windows-1254",
-
"windows-1254"=>"windows-1254",
-
"x-cp1254"=>"windows-1254",
-
"cp1255"=>"windows-1255",
-
"windows-1255"=>"windows-1255",
-
"x-cp1255"=>"windows-1255",
-
"cp1256"=>"windows-1256",
-
"windows-1256"=>"windows-1256",
-
"x-cp1256"=>"windows-1256",
-
"cp1257"=>"windows-1257",
-
"windows-1257"=>"windows-1257",
-
"x-cp1257"=>"windows-1257",
-
"cp1258"=>"windows-1258",
-
"windows-1258"=>"windows-1258",
-
"x-cp1258"=>"windows-1258",
-
"x-mac-cyrillic"=>"macCyrillic",
-
"x-mac-ukrainian"=>"macCyrillic",
-
"chinese"=>"gbk",
-
"csgb2312"=>"gbk",
-
"csiso58gb231280"=>"gbk",
-
"gb2312"=>"gbk",
-
"gb_2312"=>"gbk",
-
"gb_2312-80"=>"gbk",
-
"gbk"=>"gbk",
-
"iso-ir-58"=>"gbk",
-
"x-gbk"=>"gbk",
-
"gb18030"=>"gb18030",
-
"big5"=>"big5",
-
"big5-hkscs"=>"big5",
-
"cn-big5"=>"big5",
-
"csbig5"=>"big5",
-
"x-x-big5"=>"big5",
-
"cseucpkdfmtjapanese"=>"cp51932",
-
"euc-jp"=>"cp51932",
-
"x-euc-jp"=>"cp51932",
-
"csiso2022jp"=>"cp50221",
-
"iso-2022-jp"=>"cp50221",
-
"csshiftjis"=>"Windows-31J",
-
"ms932"=>"Windows-31J",
-
"ms_kanji"=>"Windows-31J",
-
"shift-jis"=>"Windows-31J",
-
"shift_jis"=>"Windows-31J",
-
"sjis"=>"Windows-31J",
-
"windows-31j"=>"Windows-31J",
-
"x-sjis"=>"Windows-31J",
-
"cseuckr"=>"euc-kr",
-
"csksc56011987"=>"euc-kr",
-
"euc-kr"=>"euc-kr",
-
"iso-ir-149"=>"euc-kr",
-
"korean"=>"euc-kr",
-
"ks_c_5601-1987"=>"euc-kr",
-
"ks_c_5601-1989"=>"euc-kr",
-
"ksc5601"=>"euc-kr",
-
"ksc_5601"=>"euc-kr",
-
"windows-949"=>"euc-kr",
-
"utf-16be"=>"utf-16be",
-
"utf-16"=>"utf-16le",
-
"utf-16le"=>"utf-16le",
-
} # :nodoc:
-
-
# :nodoc:
-
# return encoding or nil
-
# http://encoding.spec.whatwg.org/#concept-encoding-get
-
1
def self.get_encoding(label)
-
Encoding.find(WEB_ENCODINGS_[label.to_str.strip.downcase]) rescue nil
-
end
-
end # module URI
-
-
1
module Kernel
-
-
#
-
# Returns +uri+ converted to an URI object.
-
#
-
1
def URI(uri)
-
if uri.is_a?(URI::Generic)
-
uri
-
elsif uri = String.try_convert(uri)
-
URI.parse(uri)
-
else
-
raise ArgumentError,
-
"bad argument (expected URI object or URI string)"
-
end
-
end
-
1
module_function :URI
-
end
-
# frozen_string_literal: true
-
-
1
require_relative 'generic'
-
-
1
module URI
-
-
#
-
# The "file" URI is defined by RFC8089.
-
#
-
1
class File < Generic
-
# A Default port of nil for URI::File.
-
1
DEFAULT_PORT = nil
-
-
#
-
# An Array of the available components for URI::File.
-
#
-
1
COMPONENT = [
-
:scheme,
-
:host,
-
:path
-
].freeze
-
-
#
-
# == Description
-
#
-
# Creates a new URI::File object from components, with syntax checking.
-
#
-
# The components accepted are +host+ and +path+.
-
#
-
# The components should be provided either as an Array, or as a Hash
-
# with keys formed by preceding the component names with a colon.
-
#
-
# If an Array is used, the components must be passed in the
-
# order <code>[host, path]</code>.
-
#
-
# Examples:
-
#
-
# require 'uri'
-
#
-
# uri1 = URI::File.build(['host.example.com', '/path/file.zip'])
-
# uri1.to_s # => "file://host.example.com/path/file.zip"
-
#
-
# uri2 = URI::File.build({:host => 'host.example.com',
-
# :path => '/ruby/src'})
-
# uri2.to_s # => "file://host.example.com/ruby/src"
-
#
-
1
def self.build(args)
-
tmp = Util::make_components_hash(self, args)
-
super(tmp)
-
end
-
-
# Protected setter for the host component +v+.
-
#
-
# See also URI::Generic.host=.
-
#
-
1
def set_host(v)
-
v = "" if v.nil? || v == "localhost"
-
@host = v
-
end
-
-
# do nothing
-
1
def set_port(v)
-
end
-
-
# raise InvalidURIError
-
1
def check_userinfo(user)
-
raise URI::InvalidURIError, "can not set userinfo for file URI"
-
end
-
-
# raise InvalidURIError
-
1
def check_user(user)
-
raise URI::InvalidURIError, "can not set user for file URI"
-
end
-
-
# raise InvalidURIError
-
1
def check_password(user)
-
raise URI::InvalidURIError, "can not set password for file URI"
-
end
-
-
# do nothing
-
1
def set_userinfo(v)
-
end
-
-
# do nothing
-
1
def set_user(v)
-
end
-
-
# do nothing
-
1
def set_password(v)
-
end
-
end
-
-
1
@@schemes['FILE'] = File
-
end
-
# frozen_string_literal: false
-
# = uri/ftp.rb
-
#
-
# Author:: Akira Yamada <akira@ruby-lang.org>
-
# License:: You can redistribute it and/or modify it under the same term as Ruby.
-
# Revision:: $Id$
-
#
-
# See URI for general documentation
-
#
-
-
1
require_relative 'generic'
-
-
1
module URI
-
-
#
-
# FTP URI syntax is defined by RFC1738 section 3.2.
-
#
-
# This class will be redesigned because of difference of implementations;
-
# the structure of its path. draft-hoffman-ftp-uri-04 is a draft but it
-
# is a good summary about the de facto spec.
-
# http://tools.ietf.org/html/draft-hoffman-ftp-uri-04
-
#
-
1
class FTP < Generic
-
# A Default port of 21 for URI::FTP.
-
1
DEFAULT_PORT = 21
-
-
#
-
# An Array of the available components for URI::FTP.
-
#
-
1
COMPONENT = [
-
:scheme,
-
:userinfo, :host, :port,
-
:path, :typecode
-
].freeze
-
-
#
-
# Typecode is "a", "i", or "d".
-
#
-
# * "a" indicates a text file (the FTP command was ASCII)
-
# * "i" indicates a binary file (FTP command IMAGE)
-
# * "d" indicates the contents of a directory should be displayed
-
#
-
1
TYPECODE = ['a', 'i', 'd'].freeze
-
-
# Typecode prefix ";type=".
-
1
TYPECODE_PREFIX = ';type='.freeze
-
-
1
def self.new2(user, password, host, port, path,
-
typecode = nil, arg_check = true) # :nodoc:
-
# Do not use this method! Not tested. [Bug #7301]
-
# This methods remains just for compatibility,
-
# Keep it undocumented until the active maintainer is assigned.
-
typecode = nil if typecode.size == 0
-
if typecode && !TYPECODE.include?(typecode)
-
raise ArgumentError,
-
"bad typecode is specified: #{typecode}"
-
end
-
-
# do escape
-
-
self.new('ftp',
-
[user, password],
-
host, port, nil,
-
typecode ? path + TYPECODE_PREFIX + typecode : path,
-
nil, nil, nil, arg_check)
-
end
-
-
#
-
# == Description
-
#
-
# Creates a new URI::FTP object from components, with syntax checking.
-
#
-
# The components accepted are +userinfo+, +host+, +port+, +path+, and
-
# +typecode+.
-
#
-
# The components should be provided either as an Array, or as a Hash
-
# with keys formed by preceding the component names with a colon.
-
#
-
# If an Array is used, the components must be passed in the
-
# order <code>[userinfo, host, port, path, typecode]</code>.
-
#
-
# If the path supplied is absolute, it will be escaped in order to
-
# make it absolute in the URI.
-
#
-
# Examples:
-
#
-
# require 'uri'
-
#
-
# uri1 = URI::FTP.build(['user:password', 'ftp.example.com', nil,
-
# '/path/file.zip', 'i'])
-
# uri1.to_s # => "ftp://user:password@ftp.example.com/%2Fpath/file.zip;type=i"
-
#
-
# uri2 = URI::FTP.build({:host => 'ftp.example.com',
-
# :path => 'ruby/src'})
-
# uri2.to_s # => "ftp://ftp.example.com/ruby/src"
-
#
-
1
def self.build(args)
-
-
# Fix the incoming path to be generic URL syntax
-
# FTP path -> URL path
-
# foo/bar /foo/bar
-
# /foo/bar /%2Ffoo/bar
-
#
-
if args.kind_of?(Array)
-
args[3] = '/' + args[3].sub(/^\//, '%2F')
-
else
-
args[:path] = '/' + args[:path].sub(/^\//, '%2F')
-
end
-
-
tmp = Util::make_components_hash(self, args)
-
-
if tmp[:typecode]
-
if tmp[:typecode].size == 1
-
tmp[:typecode] = TYPECODE_PREFIX + tmp[:typecode]
-
end
-
tmp[:path] << tmp[:typecode]
-
end
-
-
return super(tmp)
-
end
-
-
#
-
# == Description
-
#
-
# Creates a new URI::FTP object from generic URL components with no
-
# syntax checking.
-
#
-
# Unlike build(), this method does not escape the path component as
-
# required by RFC1738; instead it is treated as per RFC2396.
-
#
-
# Arguments are +scheme+, +userinfo+, +host+, +port+, +registry+, +path+,
-
# +opaque+, +query+, and +fragment+, in that order.
-
#
-
1
def initialize(scheme,
-
userinfo, host, port, registry,
-
path, opaque,
-
query,
-
fragment,
-
parser = nil,
-
arg_check = false)
-
raise InvalidURIError unless path
-
path = path.sub(/^\//,'')
-
path.sub!(/^%2F/,'/')
-
super(scheme, userinfo, host, port, registry, path, opaque,
-
query, fragment, parser, arg_check)
-
@typecode = nil
-
if tmp = @path.index(TYPECODE_PREFIX)
-
typecode = @path[tmp + TYPECODE_PREFIX.size..-1]
-
@path = @path[0..tmp - 1]
-
-
if arg_check
-
self.typecode = typecode
-
else
-
self.set_typecode(typecode)
-
end
-
end
-
end
-
-
# typecode accessor.
-
#
-
# See URI::FTP::COMPONENT.
-
1
attr_reader :typecode
-
-
# Validates typecode +v+,
-
# returns +true+ or +false+.
-
#
-
1
def check_typecode(v)
-
if TYPECODE.include?(v)
-
return true
-
else
-
raise InvalidComponentError,
-
"bad typecode(expected #{TYPECODE.join(', ')}): #{v}"
-
end
-
end
-
1
private :check_typecode
-
-
# Private setter for the typecode +v+.
-
#
-
# See also URI::FTP.typecode=.
-
#
-
1
def set_typecode(v)
-
@typecode = v
-
end
-
1
protected :set_typecode
-
-
#
-
# == Args
-
#
-
# +v+::
-
# String
-
#
-
# == Description
-
#
-
# Public setter for the typecode +v+
-
# (with validation).
-
#
-
# See also URI::FTP.check_typecode.
-
#
-
# == Usage
-
#
-
# require 'uri'
-
#
-
# uri = URI.parse("ftp://john@ftp.example.com/my_file.img")
-
# #=> #<URI::FTP ftp://john@ftp.example.com/my_file.img>
-
# uri.typecode = "i"
-
# uri
-
# #=> #<URI::FTP ftp://john@ftp.example.com/my_file.img;type=i>
-
#
-
1
def typecode=(typecode)
-
check_typecode(typecode)
-
set_typecode(typecode)
-
typecode
-
end
-
-
1
def merge(oth) # :nodoc:
-
tmp = super(oth)
-
if self != tmp
-
tmp.set_typecode(oth.typecode)
-
end
-
-
return tmp
-
end
-
-
# Returns the path from an FTP URI.
-
#
-
# RFC 1738 specifically states that the path for an FTP URI does not
-
# include the / which separates the URI path from the URI host. Example:
-
#
-
# <code>ftp://ftp.example.com/pub/ruby</code>
-
#
-
# The above URI indicates that the client should connect to
-
# ftp.example.com then cd to pub/ruby from the initial login directory.
-
#
-
# If you want to cd to an absolute directory, you must include an
-
# escaped / (%2F) in the path. Example:
-
#
-
# <code>ftp://ftp.example.com/%2Fpub/ruby</code>
-
#
-
# This method will then return "/pub/ruby".
-
#
-
1
def path
-
return @path.sub(/^\//,'').sub(/^%2F/,'/')
-
end
-
-
# Private setter for the path of the URI::FTP.
-
1
def set_path(v)
-
super("/" + v.sub(/^\//, "%2F"))
-
end
-
1
protected :set_path
-
-
# Returns a String representation of the URI::FTP.
-
1
def to_s
-
save_path = nil
-
if @typecode
-
save_path = @path
-
@path = @path + TYPECODE_PREFIX + @typecode
-
end
-
str = super
-
if @typecode
-
@path = save_path
-
end
-
-
return str
-
end
-
end
-
1
@@schemes['FTP'] = FTP
-
end
-
# frozen_string_literal: true
-
-
# = uri/generic.rb
-
#
-
# Author:: Akira Yamada <akira@ruby-lang.org>
-
# License:: You can redistribute it and/or modify it under the same term as Ruby.
-
# Revision:: $Id$
-
#
-
# See URI for general documentation
-
#
-
-
1
require_relative 'common'
-
1
autoload :IPSocket, 'socket'
-
1
autoload :IPAddr, 'ipaddr'
-
-
1
module URI
-
-
#
-
# Base class for all URI classes.
-
# Implements generic URI syntax as per RFC 2396.
-
#
-
1
class Generic
-
1
include URI
-
-
#
-
# A Default port of nil for URI::Generic.
-
#
-
1
DEFAULT_PORT = nil
-
-
#
-
# Returns default port.
-
#
-
1
def self.default_port
-
9
self::DEFAULT_PORT
-
end
-
-
#
-
# Returns default port.
-
#
-
1
def default_port
-
9
self.class.default_port
-
end
-
-
#
-
# An Array of the available components for URI::Generic.
-
#
-
1
COMPONENT = [
-
:scheme,
-
:userinfo, :host, :port, :registry,
-
:path, :opaque,
-
:query,
-
:fragment
-
].freeze
-
-
#
-
# Components of the URI in the order.
-
#
-
1
def self.component
-
self::COMPONENT
-
end
-
-
1
USE_REGISTRY = false # :nodoc:
-
-
1
def self.use_registry # :nodoc:
-
self::USE_REGISTRY
-
end
-
-
#
-
# == Synopsis
-
#
-
# See ::new.
-
#
-
# == Description
-
#
-
# At first, tries to create a new URI::Generic instance using
-
# URI::Generic::build. But, if exception URI::InvalidComponentError is raised,
-
# then it does URI::Escape.escape all URI components and tries again.
-
#
-
1
def self.build2(args)
-
begin
-
return self.build(args)
-
rescue InvalidComponentError
-
if args.kind_of?(Array)
-
return self.build(args.collect{|x|
-
if x.is_a?(String)
-
DEFAULT_PARSER.escape(x)
-
else
-
x
-
end
-
})
-
elsif args.kind_of?(Hash)
-
tmp = {}
-
args.each do |key, value|
-
tmp[key] = if value
-
DEFAULT_PARSER.escape(value)
-
else
-
value
-
end
-
end
-
return self.build(tmp)
-
end
-
end
-
end
-
-
#
-
# == Synopsis
-
#
-
# See ::new.
-
#
-
# == Description
-
#
-
# Creates a new URI::Generic instance from components of URI::Generic
-
# with check. Components are: scheme, userinfo, host, port, registry, path,
-
# opaque, query, and fragment. You can provide arguments either by an Array or a Hash.
-
# See ::new for hash keys to use or for order of array items.
-
#
-
1
def self.build(args)
-
if args.kind_of?(Array) &&
-
args.size == ::URI::Generic::COMPONENT.size
-
tmp = args.dup
-
elsif args.kind_of?(Hash)
-
tmp = ::URI::Generic::COMPONENT.collect do |c|
-
if args.include?(c)
-
args[c]
-
else
-
nil
-
end
-
end
-
else
-
component = self.class.component rescue ::URI::Generic::COMPONENT
-
raise ArgumentError,
-
"expected Array of or Hash of components of #{self.class} (#{component.join(', ')})"
-
end
-
-
tmp << nil
-
tmp << true
-
return self.new(*tmp)
-
end
-
-
#
-
# == Args
-
#
-
# +scheme+::
-
# Protocol scheme, i.e. 'http','ftp','mailto' and so on.
-
# +userinfo+::
-
# User name and password, i.e. 'sdmitry:bla'.
-
# +host+::
-
# Server host name.
-
# +port+::
-
# Server port.
-
# +registry+::
-
# Registry of naming authorities.
-
# +path+::
-
# Path on server.
-
# +opaque+::
-
# Opaque part.
-
# +query+::
-
# Query data.
-
# +fragment+::
-
# Part of the URI after '#' character.
-
# +parser+::
-
# Parser for internal use [URI::DEFAULT_PARSER by default].
-
# +arg_check+::
-
# Check arguments [false by default].
-
#
-
# == Description
-
#
-
# Creates a new URI::Generic instance from ``generic'' components without check.
-
#
-
1
def initialize(scheme,
-
userinfo, host, port, registry,
-
path, opaque,
-
query,
-
fragment,
-
parser = DEFAULT_PARSER,
-
arg_check = false)
-
6
@scheme = nil
-
6
@user = nil
-
6
@password = nil
-
6
@host = nil
-
6
@port = nil
-
6
@path = nil
-
6
@query = nil
-
6
@opaque = nil
-
6
@fragment = nil
-
6
@parser = parser == DEFAULT_PARSER ? nil : parser
-
-
6
if arg_check
-
self.scheme = scheme
-
self.userinfo = userinfo
-
self.hostname = host
-
self.port = port
-
self.path = path
-
self.query = query
-
self.opaque = opaque
-
self.fragment = fragment
-
else
-
6
self.set_scheme(scheme)
-
6
self.set_userinfo(userinfo)
-
6
self.set_host(host)
-
6
self.set_port(port)
-
6
self.set_path(path)
-
6
self.query = query
-
6
self.set_opaque(opaque)
-
6
self.fragment=(fragment)
-
end
-
6
if registry
-
raise InvalidURIError,
-
"the scheme #{@scheme} does not accept registry part: #{registry} (or bad hostname?)"
-
end
-
-
6
@scheme&.freeze
-
6
self.set_path('') if !@path && !@opaque # (see RFC2396 Section 5.2)
-
6
self.set_port(self.default_port) if self.default_port && !@port
-
end
-
-
#
-
# Returns the scheme component of the URI.
-
#
-
# URI("http://foo/bar/baz").scheme #=> "http"
-
#
-
1
attr_reader :scheme
-
-
# Returns the host component of the URI.
-
#
-
# URI("http://foo/bar/baz").host #=> "foo"
-
#
-
# It returns nil if no host component exists.
-
#
-
# URI("mailto:foo@example.org").host #=> nil
-
#
-
# The component does not contain the port number.
-
#
-
# URI("http://foo:8080/bar/baz").host #=> "foo"
-
#
-
# Since IPv6 addresses are wrapped with brackets in URIs,
-
# this method returns IPv6 addresses wrapped with brackets.
-
# This form is not appropriate to pass to socket methods such as TCPSocket.open.
-
# If unwrapped host names are required, use the #hostname method.
-
#
-
# URI("http://[::1]/bar/baz").host #=> "[::1]"
-
# URI("http://[::1]/bar/baz").hostname #=> "::1"
-
#
-
1
attr_reader :host
-
-
# Returns the port component of the URI.
-
#
-
# URI("http://foo/bar/baz").port #=> 80
-
# URI("http://foo:8080/bar/baz").port #=> 8080
-
#
-
1
attr_reader :port
-
-
1
def registry # :nodoc:
-
nil
-
end
-
-
# Returns the path component of the URI.
-
#
-
# URI("http://foo/bar/baz").path #=> "/bar/baz"
-
#
-
1
attr_reader :path
-
-
# Returns the query component of the URI.
-
#
-
# URI("http://foo/bar/baz?search=FooBar").query #=> "search=FooBar"
-
#
-
1
attr_reader :query
-
-
# Returns the opaque part of the URI.
-
#
-
# URI("mailto:foo@example.org").opaque #=> "foo@example.org"
-
# URI("http://foo/bar/baz").opaque #=> nil
-
#
-
# The portion of the path that does not make use of the slash '/'.
-
# The path typically refers to an absolute path or an opaque part.
-
# (See RFC2396 Section 3 and 5.2.)
-
#
-
1
attr_reader :opaque
-
-
# Returns the fragment component of the URI.
-
#
-
# URI("http://foo/bar/baz?search=FooBar#ponies").fragment #=> "ponies"
-
#
-
1
attr_reader :fragment
-
-
# Returns the parser to be used.
-
#
-
# Unless a URI::Parser is defined, DEFAULT_PARSER is used.
-
#
-
1
def parser
-
3
if !defined?(@parser) || !@parser
-
DEFAULT_PARSER
-
else
-
3
@parser || DEFAULT_PARSER
-
end
-
end
-
-
# Replaces self by other URI object.
-
#
-
1
def replace!(oth)
-
if self.class != oth.class
-
raise ArgumentError, "expected #{self.class} object"
-
end
-
-
component.each do |c|
-
self.__send__("#{c}=", oth.__send__(c))
-
end
-
end
-
1
private :replace!
-
-
#
-
# Components of the URI in the order.
-
#
-
1
def component
-
self.class.component
-
end
-
-
#
-
# Checks the scheme +v+ component against the URI::Parser Regexp for :SCHEME.
-
#
-
1
def check_scheme(v)
-
if v && parser.regexp[:SCHEME] !~ v
-
raise InvalidComponentError,
-
"bad component(expected scheme component): #{v}"
-
end
-
-
return true
-
end
-
1
private :check_scheme
-
-
# Protected setter for the scheme component +v+.
-
#
-
# See also URI::Generic.scheme=.
-
#
-
1
def set_scheme(v)
-
6
@scheme = v&.downcase
-
end
-
1
protected :set_scheme
-
-
#
-
# == Args
-
#
-
# +v+::
-
# String
-
#
-
# == Description
-
#
-
# Public setter for the scheme component +v+
-
# (with validation).
-
#
-
# See also URI::Generic.check_scheme.
-
#
-
# == Usage
-
#
-
# require 'uri'
-
#
-
# uri = URI.parse("http://my.example.com")
-
# uri.scheme = "https"
-
# uri.to_s #=> "https://my.example.com"
-
#
-
1
def scheme=(v)
-
check_scheme(v)
-
set_scheme(v)
-
v
-
end
-
-
#
-
# Checks the +user+ and +password+.
-
#
-
# If +password+ is not provided, then +user+ is
-
# split, using URI::Generic.split_userinfo, to
-
# pull +user+ and +password.
-
#
-
# See also URI::Generic.check_user, URI::Generic.check_password.
-
#
-
1
def check_userinfo(user, password = nil)
-
if !password
-
user, password = split_userinfo(user)
-
end
-
check_user(user)
-
check_password(password, user)
-
-
return true
-
end
-
1
private :check_userinfo
-
-
#
-
# Checks the user +v+ component for RFC2396 compliance
-
# and against the URI::Parser Regexp for :USERINFO.
-
#
-
# Can not have a registry or opaque component defined,
-
# with a user component defined.
-
#
-
1
def check_user(v)
-
if @opaque
-
raise InvalidURIError,
-
"can not set user with opaque"
-
end
-
-
return v unless v
-
-
if parser.regexp[:USERINFO] !~ v
-
raise InvalidComponentError,
-
"bad component(expected userinfo component or user component): #{v}"
-
end
-
-
return true
-
end
-
1
private :check_user
-
-
#
-
# Checks the password +v+ component for RFC2396 compliance
-
# and against the URI::Parser Regexp for :USERINFO.
-
#
-
# Can not have a registry or opaque component defined,
-
# with a user component defined.
-
#
-
1
def check_password(v, user = @user)
-
if @opaque
-
raise InvalidURIError,
-
"can not set password with opaque"
-
end
-
return v unless v
-
-
if !user
-
raise InvalidURIError,
-
"password component depends user component"
-
end
-
-
if parser.regexp[:USERINFO] !~ v
-
raise InvalidComponentError,
-
"bad password component"
-
end
-
-
return true
-
end
-
1
private :check_password
-
-
#
-
# Sets userinfo, argument is string like 'name:pass'.
-
#
-
1
def userinfo=(userinfo)
-
if userinfo.nil?
-
return nil
-
end
-
check_userinfo(*userinfo)
-
set_userinfo(*userinfo)
-
# returns userinfo
-
end
-
-
#
-
# == Args
-
#
-
# +v+::
-
# String
-
#
-
# == Description
-
#
-
# Public setter for the +user+ component
-
# (with validation).
-
#
-
# See also URI::Generic.check_user.
-
#
-
# == Usage
-
#
-
# require 'uri'
-
#
-
# uri = URI.parse("http://john:S3nsit1ve@my.example.com")
-
# uri.user = "sam"
-
# uri.to_s #=> "http://sam:V3ry_S3nsit1ve@my.example.com"
-
#
-
1
def user=(user)
-
check_user(user)
-
set_user(user)
-
# returns user
-
end
-
-
#
-
# == Args
-
#
-
# +v+::
-
# String
-
#
-
# == Description
-
#
-
# Public setter for the +password+ component
-
# (with validation).
-
#
-
# See also URI::Generic.check_password.
-
#
-
# == Usage
-
#
-
# require 'uri'
-
#
-
# uri = URI.parse("http://john:S3nsit1ve@my.example.com")
-
# uri.password = "V3ry_S3nsit1ve"
-
# uri.to_s #=> "http://john:V3ry_S3nsit1ve@my.example.com"
-
#
-
1
def password=(password)
-
check_password(password)
-
set_password(password)
-
# returns password
-
end
-
-
# Protected setter for the +user+ component, and +password+ if available
-
# (with validation).
-
#
-
# See also URI::Generic.userinfo=.
-
#
-
1
def set_userinfo(user, password = nil)
-
6
unless password
-
6
user, password = split_userinfo(user)
-
end
-
6
@user = user
-
6
@password = password if password
-
-
6
[@user, @password]
-
end
-
1
protected :set_userinfo
-
-
# Protected setter for the user component +v+.
-
#
-
# See also URI::Generic.user=.
-
#
-
1
def set_user(v)
-
set_userinfo(v, @password)
-
v
-
end
-
1
protected :set_user
-
-
# Protected setter for the password component +v+.
-
#
-
# See also URI::Generic.password=.
-
#
-
1
def set_password(v)
-
@password = v
-
# returns v
-
end
-
1
protected :set_password
-
-
# Returns the userinfo +ui+ as <code>[user, password]</code>
-
# if properly formatted as 'user:password'.
-
1
def split_userinfo(ui)
-
6
return nil, nil unless ui
-
user, password = ui.split(':', 2)
-
-
return user, password
-
end
-
1
private :split_userinfo
-
-
# Escapes 'user:password' +v+ based on RFC 1738 section 3.1.
-
1
def escape_userpass(v)
-
parser.escape(v, /[@:\/]/o) # RFC 1738 section 3.1 #/
-
end
-
1
private :escape_userpass
-
-
# Returns the userinfo, either as 'user' or 'user:password'.
-
1
def userinfo
-
if @user.nil?
-
nil
-
elsif @password.nil?
-
@user
-
else
-
@user + ':' + @password
-
end
-
end
-
-
# Returns the user component.
-
1
def user
-
@user
-
end
-
-
# Returns the password component.
-
1
def password
-
@password
-
end
-
-
#
-
# Checks the host +v+ component for RFC2396 compliance
-
# and against the URI::Parser Regexp for :HOST.
-
#
-
# Can not have a registry or opaque component defined,
-
# with a host component defined.
-
#
-
1
def check_host(v)
-
3
return v unless v
-
-
3
if @opaque
-
raise InvalidURIError,
-
"can not set host with registry or opaque"
-
3
elsif parser.regexp[:HOST] !~ v
-
raise InvalidComponentError,
-
"bad component(expected host component): #{v}"
-
end
-
-
3
return true
-
end
-
1
private :check_host
-
-
# Protected setter for the host component +v+.
-
#
-
# See also URI::Generic.host=.
-
#
-
1
def set_host(v)
-
9
@host = v
-
end
-
1
protected :set_host
-
-
#
-
# == Args
-
#
-
# +v+::
-
# String
-
#
-
# == Description
-
#
-
# Public setter for the host component +v+
-
# (with validation).
-
#
-
# See also URI::Generic.check_host.
-
#
-
# == Usage
-
#
-
# require 'uri'
-
#
-
# uri = URI.parse("http://my.example.com")
-
# uri.host = "foo.com"
-
# uri.to_s #=> "http://foo.com"
-
#
-
1
def host=(v)
-
3
check_host(v)
-
3
set_host(v)
-
3
v
-
end
-
-
# Extract the host part of the URI and unwrap brackets for IPv6 addresses.
-
#
-
# This method is the same as URI::Generic#host except
-
# brackets for IPv6 (and future IP) addresses are removed.
-
#
-
# uri = URI("http://[::1]/bar")
-
# uri.hostname #=> "::1"
-
# uri.host #=> "[::1]"
-
#
-
1
def hostname
-
6
v = self.host
-
6
/\A\[(.*)\]\z/ =~ v ? $1 : v
-
end
-
-
# Sets the host part of the URI as the argument with brackets for IPv6 addresses.
-
#
-
# This method is the same as URI::Generic#host= except
-
# the argument can be a bare IPv6 address.
-
#
-
# uri = URI("http://foo/bar")
-
# uri.hostname = "::1"
-
# uri.to_s #=> "http://[::1]/bar"
-
#
-
# If the argument seems to be an IPv6 address,
-
# it is wrapped with brackets.
-
#
-
1
def hostname=(v)
-
v = "[#{v}]" if /\A\[.*\]\z/ !~ v && /:/ =~ v
-
self.host = v
-
end
-
-
#
-
# Checks the port +v+ component for RFC2396 compliance
-
# and against the URI::Parser Regexp for :PORT.
-
#
-
# Can not have a registry or opaque component defined,
-
# with a port component defined.
-
#
-
1
def check_port(v)
-
3
return v unless v
-
-
3
if @opaque
-
raise InvalidURIError,
-
"can not set port with registry or opaque"
-
3
elsif !v.kind_of?(Integer) && parser.regexp[:PORT] !~ v
-
raise InvalidComponentError,
-
"bad component(expected port component): #{v.inspect}"
-
end
-
-
3
return true
-
end
-
1
private :check_port
-
-
# Protected setter for the port component +v+.
-
#
-
# See also URI::Generic.port=.
-
#
-
1
def set_port(v)
-
9
v = v.empty? ? nil : v.to_i unless !v || v.kind_of?(Integer)
-
9
@port = v
-
end
-
1
protected :set_port
-
-
#
-
# == Args
-
#
-
# +v+::
-
# String
-
#
-
# == Description
-
#
-
# Public setter for the port component +v+
-
# (with validation).
-
#
-
# See also URI::Generic.check_port.
-
#
-
# == Usage
-
#
-
# require 'uri'
-
#
-
# uri = URI.parse("http://my.example.com")
-
# uri.port = 8080
-
# uri.to_s #=> "http://my.example.com:8080"
-
#
-
1
def port=(v)
-
3
check_port(v)
-
3
set_port(v)
-
3
port
-
end
-
-
1
def check_registry(v) # :nodoc:
-
raise InvalidURIError, "can not set registry"
-
end
-
1
private :check_registry
-
-
1
def set_registry(v) #:nodoc:
-
raise InvalidURIError, "can not set registry"
-
end
-
1
protected :set_registry
-
-
1
def registry=(v)
-
raise InvalidURIError, "can not set registry"
-
end
-
-
#
-
# Checks the path +v+ component for RFC2396 compliance
-
# and against the URI::Parser Regexp
-
# for :ABS_PATH and :REL_PATH.
-
#
-
# Can not have a opaque component defined,
-
# with a path component defined.
-
#
-
1
def check_path(v)
-
# raise if both hier and opaque are not nil, because:
-
# absoluteURI = scheme ":" ( hier_part | opaque_part )
-
# hier_part = ( net_path | abs_path ) [ "?" query ]
-
if v && @opaque
-
raise InvalidURIError,
-
"path conflicts with opaque"
-
end
-
-
# If scheme is ftp, path may be relative.
-
# See RFC 1738 section 3.2.2, and RFC 2396.
-
if @scheme && @scheme != "ftp"
-
if v && v != '' && parser.regexp[:ABS_PATH] !~ v
-
raise InvalidComponentError,
-
"bad component(expected absolute path component): #{v}"
-
end
-
else
-
if v && v != '' && parser.regexp[:ABS_PATH] !~ v &&
-
parser.regexp[:REL_PATH] !~ v
-
raise InvalidComponentError,
-
"bad component(expected relative path component): #{v}"
-
end
-
end
-
-
return true
-
end
-
1
private :check_path
-
-
# Protected setter for the path component +v+.
-
#
-
# See also URI::Generic.path=.
-
#
-
1
def set_path(v)
-
9
@path = v
-
end
-
1
protected :set_path
-
-
#
-
# == Args
-
#
-
# +v+::
-
# String
-
#
-
# == Description
-
#
-
# Public setter for the path component +v+
-
# (with validation).
-
#
-
# See also URI::Generic.check_path.
-
#
-
# == Usage
-
#
-
# require 'uri'
-
#
-
# uri = URI.parse("http://my.example.com/pub/files")
-
# uri.path = "/faq/"
-
# uri.to_s #=> "http://my.example.com/faq/"
-
#
-
1
def path=(v)
-
check_path(v)
-
set_path(v)
-
v
-
end
-
-
#
-
# == Args
-
#
-
# +v+::
-
# String
-
#
-
# == Description
-
#
-
# Public setter for the query component +v+.
-
#
-
# == Usage
-
#
-
# require 'uri'
-
#
-
# uri = URI.parse("http://my.example.com/?id=25")
-
# uri.query = "id=1"
-
# uri.to_s #=> "http://my.example.com/?id=1"
-
#
-
1
def query=(v)
-
6
return @query = nil unless v
-
2
raise InvalidURIError, "query conflicts with opaque" if @opaque
-
-
2
x = v.to_str
-
2
v = x.dup if x.equal? v
-
2
v.encode!(Encoding::UTF_8) rescue nil
-
2
v.delete!("\t\r\n")
-
2
v.force_encoding(Encoding::ASCII_8BIT)
-
2
raise InvalidURIError, "invalid percent escape: #{$1}" if /(%\H\H)/n.match(v)
-
2
v.gsub!(/(?!%\h\h|[!$-&(-;=?-_a-~])./n.freeze){'%%%02X' % $&.ord}
-
2
v.force_encoding(Encoding::US_ASCII)
-
2
@query = v
-
end
-
-
#
-
# Checks the opaque +v+ component for RFC2396 compliance and
-
# against the URI::Parser Regexp for :OPAQUE.
-
#
-
# Can not have a host, port, user, or path component defined,
-
# with an opaque component defined.
-
#
-
1
def check_opaque(v)
-
return v unless v
-
-
# raise if both hier and opaque are not nil, because:
-
# absoluteURI = scheme ":" ( hier_part | opaque_part )
-
# hier_part = ( net_path | abs_path ) [ "?" query ]
-
if @host || @port || @user || @path # userinfo = @user + ':' + @password
-
raise InvalidURIError,
-
"can not set opaque with host, port, userinfo or path"
-
elsif v && parser.regexp[:OPAQUE] !~ v
-
raise InvalidComponentError,
-
"bad component(expected opaque component): #{v}"
-
end
-
-
return true
-
end
-
1
private :check_opaque
-
-
# Protected setter for the opaque component +v+.
-
#
-
# See also URI::Generic.opaque=.
-
#
-
1
def set_opaque(v)
-
6
@opaque = v
-
end
-
1
protected :set_opaque
-
-
#
-
# == Args
-
#
-
# +v+::
-
# String
-
#
-
# == Description
-
#
-
# Public setter for the opaque component +v+
-
# (with validation).
-
#
-
# See also URI::Generic.check_opaque.
-
#
-
1
def opaque=(v)
-
check_opaque(v)
-
set_opaque(v)
-
v
-
end
-
-
#
-
# Checks the fragment +v+ component against the URI::Parser Regexp for :FRAGMENT.
-
#
-
#
-
# == Args
-
#
-
# +v+::
-
# String
-
#
-
# == Description
-
#
-
# Public setter for the fragment component +v+
-
# (with validation).
-
#
-
# == Usage
-
#
-
# require 'uri'
-
#
-
# uri = URI.parse("http://my.example.com/?id=25#time=1305212049")
-
# uri.fragment = "time=1305212086"
-
# uri.to_s #=> "http://my.example.com/?id=25#time=1305212086"
-
#
-
1
def fragment=(v)
-
6
return @fragment = nil unless v
-
-
x = v.to_str
-
v = x.dup if x.equal? v
-
v.encode!(Encoding::UTF_8) rescue nil
-
v.delete!("\t\r\n")
-
v.force_encoding(Encoding::ASCII_8BIT)
-
v.gsub!(/(?!%\h\h|[!-~])./n){'%%%02X' % $&.ord}
-
v.force_encoding(Encoding::US_ASCII)
-
@fragment = v
-
end
-
-
#
-
# Returns true if URI is hierarchical.
-
#
-
# == Description
-
#
-
# URI has components listed in order of decreasing significance from left to right,
-
# see RFC3986 https://tools.ietf.org/html/rfc3986 1.2.3.
-
#
-
# == Usage
-
#
-
# require 'uri'
-
#
-
# uri = URI.parse("http://my.example.com/")
-
# uri.hierarchical?
-
# #=> true
-
# uri = URI.parse("mailto:joe@example.com")
-
# uri.hierarchical?
-
# #=> false
-
#
-
1
def hierarchical?
-
if @path
-
true
-
else
-
false
-
end
-
end
-
-
#
-
# Returns true if URI has a scheme (e.g. http:// or https://) specified.
-
#
-
1
def absolute?
-
3
if @scheme
-
3
true
-
else
-
false
-
end
-
end
-
1
alias absolute absolute?
-
-
#
-
# Returns true if URI does not have a scheme (e.g. http:// or https://) specified.
-
#
-
1
def relative?
-
3
!absolute?
-
end
-
-
#
-
# Returns an Array of the path split on '/'.
-
#
-
1
def split_path(path)
-
path.split("/", -1)
-
end
-
1
private :split_path
-
-
#
-
# Merges a base path +base+, with relative path +rel+,
-
# returns a modified base path.
-
#
-
1
def merge_path(base, rel)
-
-
# RFC2396, Section 5.2, 5)
-
# RFC2396, Section 5.2, 6)
-
base_path = split_path(base)
-
rel_path = split_path(rel)
-
-
# RFC2396, Section 5.2, 6), a)
-
base_path << '' if base_path.last == '..'
-
while i = base_path.index('..')
-
base_path.slice!(i - 1, 2)
-
end
-
-
if (first = rel_path.first) and first.empty?
-
base_path.clear
-
rel_path.shift
-
end
-
-
# RFC2396, Section 5.2, 6), c)
-
# RFC2396, Section 5.2, 6), d)
-
rel_path.push('') if rel_path.last == '.' || rel_path.last == '..'
-
rel_path.delete('.')
-
-
# RFC2396, Section 5.2, 6), e)
-
tmp = []
-
rel_path.each do |x|
-
if x == '..' &&
-
!(tmp.empty? || tmp.last == '..')
-
tmp.pop
-
else
-
tmp << x
-
end
-
end
-
-
add_trailer_slash = !tmp.empty?
-
if base_path.empty?
-
base_path = [''] # keep '/' for root directory
-
elsif add_trailer_slash
-
base_path.pop
-
end
-
while x = tmp.shift
-
if x == '..'
-
# RFC2396, Section 4
-
# a .. or . in an absolute path has no special meaning
-
base_path.pop if base_path.size > 1
-
else
-
# if x == '..'
-
# valid absolute (but abnormal) path "/../..."
-
# else
-
# valid absolute path
-
# end
-
base_path << x
-
tmp.each {|t| base_path << t}
-
add_trailer_slash = false
-
break
-
end
-
end
-
base_path.push('') if add_trailer_slash
-
-
return base_path.join('/')
-
end
-
1
private :merge_path
-
-
#
-
# == Args
-
#
-
# +oth+::
-
# URI or String
-
#
-
# == Description
-
#
-
# Destructive form of #merge.
-
#
-
# == Usage
-
#
-
# require 'uri'
-
#
-
# uri = URI.parse("http://my.example.com")
-
# uri.merge!("/main.rbx?page=1")
-
# uri.to_s # => "http://my.example.com/main.rbx?page=1"
-
#
-
1
def merge!(oth)
-
t = merge(oth)
-
if self == t
-
nil
-
else
-
replace!(t)
-
self
-
end
-
end
-
-
#
-
# == Args
-
#
-
# +oth+::
-
# URI or String
-
#
-
# == Description
-
#
-
# Merges two URIs.
-
#
-
# == Usage
-
#
-
# require 'uri'
-
#
-
# uri = URI.parse("http://my.example.com")
-
# uri.merge("/main.rbx?page=1")
-
# # => "http://my.example.com/main.rbx?page=1"
-
#
-
1
def merge(oth)
-
rel = parser.send(:convert_to_uri, oth)
-
-
if rel.absolute?
-
#raise BadURIError, "both URI are absolute" if absolute?
-
# hmm... should return oth for usability?
-
return rel
-
end
-
-
unless self.absolute?
-
raise BadURIError, "both URI are relative"
-
end
-
-
base = self.dup
-
-
authority = rel.userinfo || rel.host || rel.port
-
-
# RFC2396, Section 5.2, 2)
-
if (rel.path.nil? || rel.path.empty?) && !authority && !rel.query
-
base.fragment=(rel.fragment) if rel.fragment
-
return base
-
end
-
-
base.query = nil
-
base.fragment=(nil)
-
-
# RFC2396, Section 5.2, 4)
-
if !authority
-
base.set_path(merge_path(base.path, rel.path)) if base.path && rel.path
-
else
-
# RFC2396, Section 5.2, 4)
-
base.set_path(rel.path) if rel.path
-
end
-
-
# RFC2396, Section 5.2, 7)
-
base.set_userinfo(rel.userinfo) if rel.userinfo
-
base.set_host(rel.host) if rel.host
-
base.set_port(rel.port) if rel.port
-
base.query = rel.query if rel.query
-
base.fragment=(rel.fragment) if rel.fragment
-
-
return base
-
end # merge
-
1
alias + merge
-
-
# :stopdoc:
-
1
def route_from_path(src, dst)
-
case dst
-
when src
-
# RFC2396, Section 4.2
-
return ''
-
when %r{(?:\A|/)\.\.?(?:/|\z)}
-
# dst has abnormal absolute path,
-
# like "/./", "/../", "/x/../", ...
-
return dst.dup
-
end
-
-
src_path = src.scan(%r{[^/]*/})
-
dst_path = dst.scan(%r{[^/]*/?})
-
-
# discard same parts
-
while !dst_path.empty? && dst_path.first == src_path.first
-
src_path.shift
-
dst_path.shift
-
end
-
-
tmp = dst_path.join
-
-
# calculate
-
if src_path.empty?
-
if tmp.empty?
-
return './'
-
elsif dst_path.first.include?(':') # (see RFC2396 Section 5)
-
return './' + tmp
-
else
-
return tmp
-
end
-
end
-
-
return '../' * src_path.size + tmp
-
end
-
1
private :route_from_path
-
# :startdoc:
-
-
# :stopdoc:
-
1
def route_from0(oth)
-
oth = parser.send(:convert_to_uri, oth)
-
if self.relative?
-
raise BadURIError,
-
"relative URI: #{self}"
-
end
-
if oth.relative?
-
raise BadURIError,
-
"relative URI: #{oth}"
-
end
-
-
if self.scheme != oth.scheme
-
return self, self.dup
-
end
-
rel = URI::Generic.new(nil, # it is relative URI
-
self.userinfo, self.host, self.port,
-
nil, self.path, self.opaque,
-
self.query, self.fragment, parser)
-
-
if rel.userinfo != oth.userinfo ||
-
rel.host.to_s.downcase != oth.host.to_s.downcase ||
-
rel.port != oth.port
-
-
if self.userinfo.nil? && self.host.nil?
-
return self, self.dup
-
end
-
-
rel.set_port(nil) if rel.port == oth.default_port
-
return rel, rel
-
end
-
rel.set_userinfo(nil)
-
rel.set_host(nil)
-
rel.set_port(nil)
-
-
if rel.path && rel.path == oth.path
-
rel.set_path('')
-
rel.query = nil if rel.query == oth.query
-
return rel, rel
-
elsif rel.opaque && rel.opaque == oth.opaque
-
rel.set_opaque('')
-
rel.query = nil if rel.query == oth.query
-
return rel, rel
-
end
-
-
# you can modify `rel', but can not `oth'.
-
return oth, rel
-
end
-
1
private :route_from0
-
# :startdoc:
-
-
#
-
# == Args
-
#
-
# +oth+::
-
# URI or String
-
#
-
# == Description
-
#
-
# Calculates relative path from oth to self.
-
#
-
# == Usage
-
#
-
# require 'uri'
-
#
-
# uri = URI.parse('http://my.example.com/main.rbx?page=1')
-
# uri.route_from('http://my.example.com')
-
# #=> #<URI::Generic /main.rbx?page=1>
-
#
-
1
def route_from(oth)
-
# you can modify `rel', but can not `oth'.
-
begin
-
oth, rel = route_from0(oth)
-
rescue
-
raise $!.class, $!.message
-
end
-
if oth == rel
-
return rel
-
end
-
-
rel.set_path(route_from_path(oth.path, self.path))
-
if rel.path == './' && self.query
-
# "./?foo" -> "?foo"
-
rel.set_path('')
-
end
-
-
return rel
-
end
-
-
1
alias - route_from
-
-
#
-
# == Args
-
#
-
# +oth+::
-
# URI or String
-
#
-
# == Description
-
#
-
# Calculates relative path to oth from self.
-
#
-
# == Usage
-
#
-
# require 'uri'
-
#
-
# uri = URI.parse('http://my.example.com')
-
# uri.route_to('http://my.example.com/main.rbx?page=1')
-
# #=> #<URI::Generic /main.rbx?page=1>
-
#
-
1
def route_to(oth)
-
parser.send(:convert_to_uri, oth).route_from(self)
-
end
-
-
#
-
# Returns normalized URI.
-
#
-
# require 'uri'
-
#
-
# URI("HTTP://my.EXAMPLE.com").normalize
-
# #=> #<URI::HTTP http://my.example.com/>
-
#
-
# Normalization here means:
-
#
-
# * scheme and host are converted to lowercase,
-
# * an empty path component is set to "/".
-
#
-
1
def normalize
-
uri = dup
-
uri.normalize!
-
uri
-
end
-
-
#
-
# Destructive version of #normalize.
-
#
-
1
def normalize!
-
if path&.empty?
-
set_path('/')
-
end
-
if scheme && scheme != scheme.downcase
-
set_scheme(self.scheme.downcase)
-
end
-
if host && host != host.downcase
-
set_host(self.host.downcase)
-
end
-
end
-
-
#
-
# Constructs String from URI.
-
#
-
1
def to_s
-
str = ''.dup
-
if @scheme
-
str << @scheme
-
str << ':'
-
end
-
-
if @opaque
-
str << @opaque
-
else
-
if @host || %w[file postgres].include?(@scheme)
-
str << '//'
-
end
-
if self.userinfo
-
str << self.userinfo
-
str << '@'
-
end
-
if @host
-
str << @host
-
end
-
if @port && @port != self.default_port
-
str << ':'
-
str << @port.to_s
-
end
-
str << @path
-
if @query
-
str << '?'
-
str << @query
-
end
-
end
-
if @fragment
-
str << '#'
-
str << @fragment
-
end
-
str
-
end
-
-
#
-
# Compares two URIs.
-
#
-
1
def ==(oth)
-
if self.class == oth.class
-
self.normalize.component_ary == oth.normalize.component_ary
-
else
-
false
-
end
-
end
-
-
1
def hash
-
self.component_ary.hash
-
end
-
-
1
def eql?(oth)
-
self.class == oth.class &&
-
parser == oth.parser &&
-
self.component_ary.eql?(oth.component_ary)
-
end
-
-
=begin
-
-
--- URI::Generic#===(oth)
-
-
=end
-
# def ===(oth)
-
# raise NotImplementedError
-
# end
-
-
=begin
-
=end
-
-
-
# Returns an Array of the components defined from the COMPONENT Array.
-
1
def component_ary
-
component.collect do |x|
-
self.send(x)
-
end
-
end
-
1
protected :component_ary
-
-
# == Args
-
#
-
# +components+::
-
# Multiple Symbol arguments defined in URI::HTTP.
-
#
-
# == Description
-
#
-
# Selects specified components from URI.
-
#
-
# == Usage
-
#
-
# require 'uri'
-
#
-
# uri = URI.parse('http://myuser:mypass@my.example.com/test.rbx')
-
# uri.select(:userinfo, :host, :path)
-
# # => ["myuser:mypass", "my.example.com", "/test.rbx"]
-
#
-
1
def select(*components)
-
components.collect do |c|
-
if component.include?(c)
-
self.send(c)
-
else
-
raise ArgumentError,
-
"expected of components of #{self.class} (#{self.class.component.join(', ')})"
-
end
-
end
-
end
-
-
1
def inspect
-
"#<#{self.class} #{self}>"
-
end
-
-
#
-
# == Args
-
#
-
# +v+::
-
# URI or String
-
#
-
# == Description
-
#
-
# Attempts to parse other URI +oth+,
-
# returns [parsed_oth, self].
-
#
-
# == Usage
-
#
-
# require 'uri'
-
#
-
# uri = URI.parse("http://my.example.com")
-
# uri.coerce("http://foo.com")
-
# #=> [#<URI::HTTP http://foo.com>, #<URI::HTTP http://my.example.com>]
-
#
-
1
def coerce(oth)
-
case oth
-
when String
-
oth = parser.parse(oth)
-
else
-
super
-
end
-
-
return oth, self
-
end
-
-
# Returns a proxy URI.
-
# The proxy URI is obtained from environment variables such as http_proxy,
-
# ftp_proxy, no_proxy, etc.
-
# If there is no proper proxy, nil is returned.
-
#
-
# If the optional parameter +env+ is specified, it is used instead of ENV.
-
#
-
# Note that capitalized variables (HTTP_PROXY, FTP_PROXY, NO_PROXY, etc.)
-
# are examined, too.
-
#
-
# But http_proxy and HTTP_PROXY is treated specially under CGI environment.
-
# It's because HTTP_PROXY may be set by Proxy: header.
-
# So HTTP_PROXY is not used.
-
# http_proxy is not used too if the variable is case insensitive.
-
# CGI_HTTP_PROXY can be used instead.
-
1
def find_proxy(env=ENV)
-
3
raise BadURIError, "relative URI: #{self}" if self.relative?
-
3
name = self.scheme.downcase + '_proxy'
-
3
proxy_uri = nil
-
3
if name == 'http_proxy' && env.include?('REQUEST_METHOD') # CGI?
-
# HTTP_PROXY conflicts with *_proxy for proxy settings and
-
# HTTP_* for header information in CGI.
-
# So it should be careful to use it.
-
pairs = env.reject {|k, v| /\Ahttp_proxy\z/i !~ k }
-
case pairs.length
-
when 0 # no proxy setting anyway.
-
proxy_uri = nil
-
when 1
-
k, _ = pairs.shift
-
if k == 'http_proxy' && env[k.upcase] == nil
-
# http_proxy is safe to use because ENV is case sensitive.
-
proxy_uri = env[name]
-
else
-
proxy_uri = nil
-
end
-
else # http_proxy is safe to use because ENV is case sensitive.
-
proxy_uri = env.to_hash[name]
-
end
-
if !proxy_uri
-
# Use CGI_HTTP_PROXY. cf. libwww-perl.
-
proxy_uri = env["CGI_#{name.upcase}"]
-
end
-
3
elsif name == 'http_proxy'
-
3
unless proxy_uri = env[name]
-
3
if proxy_uri = env[name.upcase]
-
warn 'The environment variable HTTP_PROXY is discouraged. Use http_proxy.', uplevel: 1
-
end
-
end
-
else
-
proxy_uri = env[name] || env[name.upcase]
-
end
-
-
3
if proxy_uri.nil? || proxy_uri.empty?
-
3
return nil
-
end
-
-
if self.hostname
-
begin
-
addr = IPSocket.getaddress(self.hostname)
-
return nil if /\A127\.|\A::1\z/ =~ addr
-
rescue SocketError
-
end
-
end
-
-
name = 'no_proxy'
-
if no_proxy = env[name] || env[name.upcase]
-
return nil unless URI::Generic.use_proxy?(self.hostname, addr, self.port, no_proxy)
-
end
-
URI.parse(proxy_uri)
-
end
-
-
1
def self.use_proxy?(hostname, addr, port, no_proxy) # :nodoc:
-
hostname = hostname.downcase
-
dothostname = ".#{hostname}"
-
no_proxy.scan(/([^:,\s]+)(?::(\d+))?/) {|p_host, p_port|
-
if !p_port || port == p_port.to_i
-
if p_host.start_with?('.')
-
return false if hostname.end_with?(p_host.downcase)
-
else
-
return false if dothostname.end_with?(".#{p_host.downcase}")
-
end
-
if addr
-
begin
-
return false if IPAddr.new(p_host).include?(addr)
-
rescue IPAddr::InvalidAddressError
-
next
-
end
-
end
-
end
-
}
-
true
-
end
-
end
-
end
-
# frozen_string_literal: false
-
# = uri/http.rb
-
#
-
# Author:: Akira Yamada <akira@ruby-lang.org>
-
# License:: You can redistribute it and/or modify it under the same term as Ruby.
-
# Revision:: $Id$
-
#
-
# See URI for general documentation
-
#
-
-
1
require_relative 'generic'
-
-
1
module URI
-
-
#
-
# The syntax of HTTP URIs is defined in RFC1738 section 3.3.
-
#
-
# Note that the Ruby URI library allows HTTP URLs containing usernames and
-
# passwords. This is not legal as per the RFC, but used to be
-
# supported in Internet Explorer 5 and 6, before the MS04-004 security
-
# update. See <URL:http://support.microsoft.com/kb/834489>.
-
#
-
1
class HTTP < Generic
-
# A Default port of 80 for URI::HTTP.
-
1
DEFAULT_PORT = 80
-
-
# An Array of the available components for URI::HTTP.
-
1
COMPONENT = %i[
-
scheme
-
userinfo host port
-
path
-
query
-
fragment
-
].freeze
-
-
#
-
# == Description
-
#
-
# Creates a new URI::HTTP object from components, with syntax checking.
-
#
-
# The components accepted are userinfo, host, port, path, query, and
-
# fragment.
-
#
-
# The components should be provided either as an Array, or as a Hash
-
# with keys formed by preceding the component names with a colon.
-
#
-
# If an Array is used, the components must be passed in the
-
# order <code>[userinfo, host, port, path, query, fragment]</code>.
-
#
-
# Example:
-
#
-
# uri = URI::HTTP.build(host: 'www.example.com', path: '/foo/bar')
-
#
-
# uri = URI::HTTP.build([nil, "www.example.com", nil, "/path",
-
# "query", 'fragment'])
-
#
-
# Currently, if passed userinfo components this method generates
-
# invalid HTTP URIs as per RFC 1738.
-
#
-
1
def self.build(args)
-
tmp = Util.make_components_hash(self, args)
-
super(tmp)
-
end
-
-
#
-
# == Description
-
#
-
# Returns the full path for an HTTP request, as required by Net::HTTP::Get.
-
#
-
# If the URI contains a query, the full path is URI#path + '?' + URI#query.
-
# Otherwise, the path is simply URI#path.
-
#
-
# Example:
-
#
-
# uri = URI::HTTP.build(path: '/foo/bar', query: 'test=true')
-
# uri.request_uri # => "/foo/bar?test=true"
-
#
-
1
def request_uri
-
3
return unless @path
-
-
3
url = @query ? "#@path?#@query" : @path.dup
-
3
url.start_with?(?/.freeze) ? url : ?/ + url
-
end
-
end
-
-
1
@@schemes['HTTP'] = HTTP
-
-
end
-
# frozen_string_literal: false
-
# = uri/https.rb
-
#
-
# Author:: Akira Yamada <akira@ruby-lang.org>
-
# License:: You can redistribute it and/or modify it under the same term as Ruby.
-
# Revision:: $Id$
-
#
-
# See URI for general documentation
-
#
-
-
1
require_relative 'http'
-
-
1
module URI
-
-
# The default port for HTTPS URIs is 443, and the scheme is 'https:' rather
-
# than 'http:'. Other than that, HTTPS URIs are identical to HTTP URIs;
-
# see URI::HTTP.
-
1
class HTTPS < HTTP
-
# A Default port of 443 for URI::HTTPS
-
1
DEFAULT_PORT = 443
-
end
-
1
@@schemes['HTTPS'] = HTTPS
-
end
-
# frozen_string_literal: false
-
# = uri/ldap.rb
-
#
-
# Author::
-
# Takaaki Tateishi <ttate@jaist.ac.jp>
-
# Akira Yamada <akira@ruby-lang.org>
-
# License::
-
# URI::LDAP is copyrighted free software by Takaaki Tateishi and Akira Yamada.
-
# You can redistribute it and/or modify it under the same term as Ruby.
-
# Revision:: $Id$
-
#
-
# See URI for general documentation
-
#
-
-
1
require_relative 'generic'
-
-
1
module URI
-
-
#
-
# LDAP URI SCHEMA (described in RFC2255).
-
#--
-
# ldap://<host>/<dn>[?<attrs>[?<scope>[?<filter>[?<extensions>]]]]
-
#++
-
1
class LDAP < Generic
-
-
# A Default port of 389 for URI::LDAP.
-
1
DEFAULT_PORT = 389
-
-
# An Array of the available components for URI::LDAP.
-
1
COMPONENT = [
-
:scheme,
-
:host, :port,
-
:dn,
-
:attributes,
-
:scope,
-
:filter,
-
:extensions,
-
].freeze
-
-
# Scopes available for the starting point.
-
#
-
# * SCOPE_BASE - the Base DN
-
# * SCOPE_ONE - one level under the Base DN, not including the base DN and
-
# not including any entries under this
-
# * SCOPE_SUB - subtrees, all entries at all levels
-
#
-
SCOPE = [
-
1
SCOPE_ONE = 'one',
-
SCOPE_SUB = 'sub',
-
SCOPE_BASE = 'base',
-
].freeze
-
-
#
-
# == Description
-
#
-
# Creates a new URI::LDAP object from components, with syntax checking.
-
#
-
# The components accepted are host, port, dn, attributes,
-
# scope, filter, and extensions.
-
#
-
# The components should be provided either as an Array, or as a Hash
-
# with keys formed by preceding the component names with a colon.
-
#
-
# If an Array is used, the components must be passed in the
-
# order <code>[host, port, dn, attributes, scope, filter, extensions]</code>.
-
#
-
# Example:
-
#
-
# uri = URI::LDAP.build({:host => 'ldap.example.com',
-
# :dn => '/dc=example'})
-
#
-
# uri = URI::LDAP.build(["ldap.example.com", nil,
-
# "/dc=example;dc=com", "query", nil, nil, nil])
-
#
-
1
def self.build(args)
-
tmp = Util::make_components_hash(self, args)
-
-
if tmp[:dn]
-
tmp[:path] = tmp[:dn]
-
end
-
-
query = []
-
[:extensions, :filter, :scope, :attributes].collect do |x|
-
next if !tmp[x] && query.size == 0
-
query.unshift(tmp[x])
-
end
-
-
tmp[:query] = query.join('?')
-
-
return super(tmp)
-
end
-
-
#
-
# == Description
-
#
-
# Creates a new URI::LDAP object from generic URI components as per
-
# RFC 2396. No LDAP-specific syntax checking is performed.
-
#
-
# Arguments are +scheme+, +userinfo+, +host+, +port+, +registry+, +path+,
-
# +opaque+, +query+, and +fragment+, in that order.
-
#
-
# Example:
-
#
-
# uri = URI::LDAP.new("ldap", nil, "ldap.example.com", nil, nil,
-
# "/dc=example;dc=com", nil, "query", nil)
-
#
-
# See also URI::Generic.new.
-
#
-
1
def initialize(*arg)
-
super(*arg)
-
-
if @fragment
-
raise InvalidURIError, 'bad LDAP URL'
-
end
-
-
parse_dn
-
parse_query
-
end
-
-
# Private method to cleanup +dn+ from using the +path+ component attribute.
-
1
def parse_dn
-
@dn = @path[1..-1]
-
end
-
1
private :parse_dn
-
-
# Private method to cleanup +attributes+, +scope+, +filter+, and +extensions+
-
# from using the +query+ component attribute.
-
1
def parse_query
-
@attributes = nil
-
@scope = nil
-
@filter = nil
-
@extensions = nil
-
-
if @query
-
attrs, scope, filter, extensions = @query.split('?')
-
-
@attributes = attrs if attrs && attrs.size > 0
-
@scope = scope if scope && scope.size > 0
-
@filter = filter if filter && filter.size > 0
-
@extensions = extensions if extensions && extensions.size > 0
-
end
-
end
-
1
private :parse_query
-
-
# Private method to assemble +query+ from +attributes+, +scope+, +filter+, and +extensions+.
-
1
def build_path_query
-
@path = '/' + @dn
-
-
query = []
-
[@extensions, @filter, @scope, @attributes].each do |x|
-
next if !x && query.size == 0
-
query.unshift(x)
-
end
-
@query = query.join('?')
-
end
-
1
private :build_path_query
-
-
# Returns dn.
-
1
def dn
-
@dn
-
end
-
-
# Private setter for dn +val+.
-
1
def set_dn(val)
-
@dn = val
-
build_path_query
-
@dn
-
end
-
1
protected :set_dn
-
-
# Setter for dn +val+.
-
1
def dn=(val)
-
set_dn(val)
-
val
-
end
-
-
# Returns attributes.
-
1
def attributes
-
@attributes
-
end
-
-
# Private setter for attributes +val+.
-
1
def set_attributes(val)
-
@attributes = val
-
build_path_query
-
@attributes
-
end
-
1
protected :set_attributes
-
-
# Setter for attributes +val+.
-
1
def attributes=(val)
-
set_attributes(val)
-
val
-
end
-
-
# Returns scope.
-
1
def scope
-
@scope
-
end
-
-
# Private setter for scope +val+.
-
1
def set_scope(val)
-
@scope = val
-
build_path_query
-
@scope
-
end
-
1
protected :set_scope
-
-
# Setter for scope +val+.
-
1
def scope=(val)
-
set_scope(val)
-
val
-
end
-
-
# Returns filter.
-
1
def filter
-
@filter
-
end
-
-
# Private setter for filter +val+.
-
1
def set_filter(val)
-
@filter = val
-
build_path_query
-
@filter
-
end
-
1
protected :set_filter
-
-
# Setter for filter +val+.
-
1
def filter=(val)
-
set_filter(val)
-
val
-
end
-
-
# Returns extensions.
-
1
def extensions
-
@extensions
-
end
-
-
# Private setter for extensions +val+.
-
1
def set_extensions(val)
-
@extensions = val
-
build_path_query
-
@extensions
-
end
-
1
protected :set_extensions
-
-
# Setter for extensions +val+.
-
1
def extensions=(val)
-
set_extensions(val)
-
val
-
end
-
-
# Checks if URI has a path.
-
# For URI::LDAP this will return +false+.
-
1
def hierarchical?
-
false
-
end
-
end
-
-
1
@@schemes['LDAP'] = LDAP
-
end
-
# frozen_string_literal: false
-
# = uri/ldap.rb
-
#
-
# License:: You can redistribute it and/or modify it under the same term as Ruby.
-
#
-
# See URI for general documentation
-
#
-
-
1
require_relative 'ldap'
-
-
1
module URI
-
-
# The default port for LDAPS URIs is 636, and the scheme is 'ldaps:' rather
-
# than 'ldap:'. Other than that, LDAPS URIs are identical to LDAP URIs;
-
# see URI::LDAP.
-
1
class LDAPS < LDAP
-
# A Default port of 636 for URI::LDAPS
-
1
DEFAULT_PORT = 636
-
end
-
1
@@schemes['LDAPS'] = LDAPS
-
end
-
# frozen_string_literal: false
-
# = uri/mailto.rb
-
#
-
# Author:: Akira Yamada <akira@ruby-lang.org>
-
# License:: You can redistribute it and/or modify it under the same term as Ruby.
-
# Revision:: $Id$
-
#
-
# See URI for general documentation
-
#
-
-
1
require_relative 'generic'
-
-
1
module URI
-
-
#
-
# RFC6068, the mailto URL scheme.
-
#
-
1
class MailTo < Generic
-
1
include REGEXP
-
-
# A Default port of nil for URI::MailTo.
-
1
DEFAULT_PORT = nil
-
-
# An Array of the available components for URI::MailTo.
-
1
COMPONENT = [ :scheme, :to, :headers ].freeze
-
-
# :stopdoc:
-
# "hname" and "hvalue" are encodings of an RFC 822 header name and
-
# value, respectively. As with "to", all URL reserved characters must
-
# be encoded.
-
#
-
# "#mailbox" is as specified in RFC 822 [RFC822]. This means that it
-
# consists of zero or more comma-separated mail addresses, possibly
-
# including "phrase" and "comment" components. Note that all URL
-
# reserved characters in "to" must be encoded: in particular,
-
# parentheses, commas, and the percent sign ("%"), which commonly occur
-
# in the "mailbox" syntax.
-
#
-
# Within mailto URLs, the characters "?", "=", "&" are reserved.
-
-
# ; RFC 6068
-
# hfields = "?" hfield *( "&" hfield )
-
# hfield = hfname "=" hfvalue
-
# hfname = *qchar
-
# hfvalue = *qchar
-
# qchar = unreserved / pct-encoded / some-delims
-
# some-delims = "!" / "$" / "'" / "(" / ")" / "*"
-
# / "+" / "," / ";" / ":" / "@"
-
#
-
# ; RFC3986
-
# unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
-
# pct-encoded = "%" HEXDIG HEXDIG
-
1
HEADER_REGEXP = /\A(?<hfield>(?:%\h\h|[!$'-.0-;@-Z_a-z~])*=(?:%\h\h|[!$'-.0-;@-Z_a-z~])*)(?:&\g<hfield>)*\z/
-
# practical regexp for email address
-
# https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address
-
1
EMAIL_REGEXP = /\A[a-zA-Z0-9.!\#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*\z/
-
# :startdoc:
-
-
#
-
# == Description
-
#
-
# Creates a new URI::MailTo object from components, with syntax checking.
-
#
-
# Components can be provided as an Array or Hash. If an Array is used,
-
# the components must be supplied as <code>[to, headers]</code>.
-
#
-
# If a Hash is used, the keys are the component names preceded by colons.
-
#
-
# The headers can be supplied as a pre-encoded string, such as
-
# <code>"subject=subscribe&cc=address"</code>, or as an Array of Arrays
-
# like <code>[['subject', 'subscribe'], ['cc', 'address']]</code>.
-
#
-
# Examples:
-
#
-
# require 'uri'
-
#
-
# m1 = URI::MailTo.build(['joe@example.com', 'subject=Ruby'])
-
# m1.to_s # => "mailto:joe@example.com?subject=Ruby"
-
#
-
# m2 = URI::MailTo.build(['john@example.com', [['Subject', 'Ruby'], ['Cc', 'jack@example.com']]])
-
# m2.to_s # => "mailto:john@example.com?Subject=Ruby&Cc=jack@example.com"
-
#
-
# m3 = URI::MailTo.build({:to => 'listman@example.com', :headers => [['subject', 'subscribe']]})
-
# m3.to_s # => "mailto:listman@example.com?subject=subscribe"
-
#
-
1
def self.build(args)
-
tmp = Util.make_components_hash(self, args)
-
-
case tmp[:to]
-
when Array
-
tmp[:opaque] = tmp[:to].join(',')
-
when String
-
tmp[:opaque] = tmp[:to].dup
-
else
-
tmp[:opaque] = ''
-
end
-
-
if tmp[:headers]
-
query =
-
case tmp[:headers]
-
when Array
-
tmp[:headers].collect { |x|
-
if x.kind_of?(Array)
-
x[0] + '=' + x[1..-1].join
-
else
-
x.to_s
-
end
-
}.join('&')
-
when Hash
-
tmp[:headers].collect { |h,v|
-
h + '=' + v
-
}.join('&')
-
else
-
tmp[:headers].to_s
-
end
-
unless query.empty?
-
tmp[:opaque] << '?' << query
-
end
-
end
-
-
super(tmp)
-
end
-
-
#
-
# == Description
-
#
-
# Creates a new URI::MailTo object from generic URL components with
-
# no syntax checking.
-
#
-
# This method is usually called from URI::parse, which checks
-
# the validity of each component.
-
#
-
1
def initialize(*arg)
-
super(*arg)
-
-
@to = nil
-
@headers = []
-
-
# The RFC3986 parser does not normally populate opaque
-
@opaque = "?#{@query}" if @query && !@opaque
-
-
unless @opaque
-
raise InvalidComponentError,
-
"missing opaque part for mailto URL"
-
end
-
to, header = @opaque.split('?', 2)
-
# allow semicolon as a addr-spec separator
-
# http://support.microsoft.com/kb/820868
-
unless /\A(?:[^@,;]+@[^@,;]+(?:\z|[,;]))*\z/ =~ to
-
raise InvalidComponentError,
-
"unrecognised opaque part for mailtoURL: #{@opaque}"
-
end
-
-
if arg[10] # arg_check
-
self.to = to
-
self.headers = header
-
else
-
set_to(to)
-
set_headers(header)
-
end
-
end
-
-
# The primary e-mail address of the URL, as a String.
-
1
attr_reader :to
-
-
# E-mail headers set by the URL, as an Array of Arrays.
-
1
attr_reader :headers
-
-
# Checks the to +v+ component.
-
1
def check_to(v)
-
return true unless v
-
return true if v.size == 0
-
-
v.split(/[,;]/).each do |addr|
-
# check url safety as path-rootless
-
if /\A(?:%\h\h|[!$&-.0-;=@-Z_a-z~])*\z/ !~ addr
-
raise InvalidComponentError,
-
"an address in 'to' is invalid as URI #{addr.dump}"
-
end
-
-
# check addr-spec
-
# don't s/\+/ /g
-
addr.gsub!(/%\h\h/, URI::TBLDECWWWCOMP_)
-
if EMAIL_REGEXP !~ addr
-
raise InvalidComponentError,
-
"an address in 'to' is invalid as uri-escaped addr-spec #{addr.dump}"
-
end
-
end
-
-
true
-
end
-
1
private :check_to
-
-
# Private setter for to +v+.
-
1
def set_to(v)
-
@to = v
-
end
-
1
protected :set_to
-
-
# Setter for to +v+.
-
1
def to=(v)
-
check_to(v)
-
set_to(v)
-
v
-
end
-
-
# Checks the headers +v+ component against either
-
# * HEADER_REGEXP
-
1
def check_headers(v)
-
return true unless v
-
return true if v.size == 0
-
if HEADER_REGEXP !~ v
-
raise InvalidComponentError,
-
"bad component(expected opaque component): #{v}"
-
end
-
-
true
-
end
-
1
private :check_headers
-
-
# Private setter for headers +v+.
-
1
def set_headers(v)
-
@headers = []
-
if v
-
v.split('&').each do |x|
-
@headers << x.split(/=/, 2)
-
end
-
end
-
end
-
1
protected :set_headers
-
-
# Setter for headers +v+.
-
1
def headers=(v)
-
check_headers(v)
-
set_headers(v)
-
v
-
end
-
-
# Constructs String from URI.
-
1
def to_s
-
@scheme + ':' +
-
if @to
-
@to
-
else
-
''
-
end +
-
if @headers.size > 0
-
'?' + @headers.collect{|x| x.join('=')}.join('&')
-
else
-
''
-
end +
-
if @fragment
-
'#' + @fragment
-
else
-
''
-
end
-
end
-
-
# Returns the RFC822 e-mail text equivalent of the URL, as a String.
-
#
-
# Example:
-
#
-
# require 'uri'
-
#
-
# uri = URI.parse("mailto:ruby-list@ruby-lang.org?Subject=subscribe&cc=myaddr")
-
# uri.to_mailtext
-
# # => "To: ruby-list@ruby-lang.org\nSubject: subscribe\nCc: myaddr\n\n\n"
-
#
-
1
def to_mailtext
-
to = URI.decode_www_form_component(@to)
-
head = ''
-
body = ''
-
@headers.each do |x|
-
case x[0]
-
when 'body'
-
body = URI.decode_www_form_component(x[1])
-
when 'to'
-
to << ', ' + URI.decode_www_form_component(x[1])
-
else
-
head << URI.decode_www_form_component(x[0]).capitalize + ': ' +
-
URI.decode_www_form_component(x[1]) + "\n"
-
end
-
end
-
-
"To: #{to}
-
#{head}
-
#{body}
-
"
-
end
-
1
alias to_rfc822text to_mailtext
-
end
-
-
1
@@schemes['MAILTO'] = MailTo
-
end
-
# frozen_string_literal: false
-
#--
-
# = uri/common.rb
-
#
-
# Author:: Akira Yamada <akira@ruby-lang.org>
-
# Revision:: $Id$
-
# License::
-
# You can redistribute it and/or modify it under the same term as Ruby.
-
#
-
# See URI for general documentation
-
#
-
-
1
module URI
-
#
-
# Includes URI::REGEXP::PATTERN
-
#
-
1
module RFC2396_REGEXP
-
#
-
# Patterns used to parse URI's
-
#
-
1
module PATTERN
-
# :stopdoc:
-
-
# RFC 2396 (URI Generic Syntax)
-
# RFC 2732 (IPv6 Literal Addresses in URL's)
-
# RFC 2373 (IPv6 Addressing Architecture)
-
-
# alpha = lowalpha | upalpha
-
1
ALPHA = "a-zA-Z"
-
# alphanum = alpha | digit
-
1
ALNUM = "#{ALPHA}\\d"
-
-
# hex = digit | "A" | "B" | "C" | "D" | "E" | "F" |
-
# "a" | "b" | "c" | "d" | "e" | "f"
-
1
HEX = "a-fA-F\\d"
-
# escaped = "%" hex hex
-
1
ESCAPED = "%[#{HEX}]{2}"
-
# mark = "-" | "_" | "." | "!" | "~" | "*" | "'" |
-
# "(" | ")"
-
# unreserved = alphanum | mark
-
1
UNRESERVED = "\\-_.!~*'()#{ALNUM}"
-
# reserved = ";" | "/" | "?" | ":" | "@" | "&" | "=" | "+" |
-
# "$" | ","
-
# reserved = ";" | "/" | "?" | ":" | "@" | "&" | "=" | "+" |
-
# "$" | "," | "[" | "]" (RFC 2732)
-
1
RESERVED = ";/?:@&=+$,\\[\\]"
-
-
# domainlabel = alphanum | alphanum *( alphanum | "-" ) alphanum
-
1
DOMLABEL = "(?:[#{ALNUM}](?:[-#{ALNUM}]*[#{ALNUM}])?)"
-
# toplabel = alpha | alpha *( alphanum | "-" ) alphanum
-
1
TOPLABEL = "(?:[#{ALPHA}](?:[-#{ALNUM}]*[#{ALNUM}])?)"
-
# hostname = *( domainlabel "." ) toplabel [ "." ]
-
1
HOSTNAME = "(?:#{DOMLABEL}\\.)*#{TOPLABEL}\\.?"
-
-
# :startdoc:
-
end # PATTERN
-
-
# :startdoc:
-
end # REGEXP
-
-
# Class that parses String's into URI's.
-
#
-
# It contains a Hash set of patterns and Regexp's that match and validate.
-
#
-
1
class RFC2396_Parser
-
1
include RFC2396_REGEXP
-
-
#
-
# == Synopsis
-
#
-
# URI::Parser.new([opts])
-
#
-
# == Args
-
#
-
# The constructor accepts a hash as options for parser.
-
# Keys of options are pattern names of URI components
-
# and values of options are pattern strings.
-
# The constructor generates set of regexps for parsing URIs.
-
#
-
# You can use the following keys:
-
#
-
# * :ESCAPED (URI::PATTERN::ESCAPED in default)
-
# * :UNRESERVED (URI::PATTERN::UNRESERVED in default)
-
# * :DOMLABEL (URI::PATTERN::DOMLABEL in default)
-
# * :TOPLABEL (URI::PATTERN::TOPLABEL in default)
-
# * :HOSTNAME (URI::PATTERN::HOSTNAME in default)
-
#
-
# == Examples
-
#
-
# p = URI::Parser.new(:ESCAPED => "(?:%[a-fA-F0-9]{2}|%u[a-fA-F0-9]{4})")
-
# u = p.parse("http://example.jp/%uABCD") #=> #<URI::HTTP http://example.jp/%uABCD>
-
# URI.parse(u.to_s) #=> raises URI::InvalidURIError
-
#
-
# s = "http://example.com/ABCD"
-
# u1 = p.parse(s) #=> #<URI::HTTP http://example.com/ABCD>
-
# u2 = URI.parse(s) #=> #<URI::HTTP http://example.com/ABCD>
-
# u1 == u2 #=> true
-
# u1.eql?(u2) #=> false
-
#
-
1
def initialize(opts = {})
-
1
@pattern = initialize_pattern(opts)
-
1
@pattern.each_value(&:freeze)
-
1
@pattern.freeze
-
-
1
@regexp = initialize_regexp(@pattern)
-
1
@regexp.each_value(&:freeze)
-
1
@regexp.freeze
-
end
-
-
# The Hash of patterns.
-
#
-
# See also URI::Parser.initialize_pattern.
-
1
attr_reader :pattern
-
-
# The Hash of Regexp.
-
#
-
# See also URI::Parser.initialize_regexp.
-
1
attr_reader :regexp
-
-
# Returns a split URI against regexp[:ABS_URI].
-
1
def split(uri)
-
case uri
-
when ''
-
# null uri
-
-
when @regexp[:ABS_URI]
-
scheme, opaque, userinfo, host, port,
-
registry, path, query, fragment = $~[1..-1]
-
-
# URI-reference = [ absoluteURI | relativeURI ] [ "#" fragment ]
-
-
# absoluteURI = scheme ":" ( hier_part | opaque_part )
-
# hier_part = ( net_path | abs_path ) [ "?" query ]
-
# opaque_part = uric_no_slash *uric
-
-
# abs_path = "/" path_segments
-
# net_path = "//" authority [ abs_path ]
-
-
# authority = server | reg_name
-
# server = [ [ userinfo "@" ] hostport ]
-
-
if !scheme
-
raise InvalidURIError,
-
"bad URI(absolute but no scheme): #{uri}"
-
end
-
if !opaque && (!path && (!host && !registry))
-
raise InvalidURIError,
-
"bad URI(absolute but no path): #{uri}"
-
end
-
-
when @regexp[:REL_URI]
-
scheme = nil
-
opaque = nil
-
-
userinfo, host, port, registry,
-
rel_segment, abs_path, query, fragment = $~[1..-1]
-
if rel_segment && abs_path
-
path = rel_segment + abs_path
-
elsif rel_segment
-
path = rel_segment
-
elsif abs_path
-
path = abs_path
-
end
-
-
# URI-reference = [ absoluteURI | relativeURI ] [ "#" fragment ]
-
-
# relativeURI = ( net_path | abs_path | rel_path ) [ "?" query ]
-
-
# net_path = "//" authority [ abs_path ]
-
# abs_path = "/" path_segments
-
# rel_path = rel_segment [ abs_path ]
-
-
# authority = server | reg_name
-
# server = [ [ userinfo "@" ] hostport ]
-
-
else
-
raise InvalidURIError, "bad URI(is not URI?): #{uri}"
-
end
-
-
path = '' if !path && !opaque # (see RFC2396 Section 5.2)
-
ret = [
-
scheme,
-
userinfo, host, port, # X
-
registry, # X
-
path, # Y
-
opaque, # Y
-
query,
-
fragment
-
]
-
return ret
-
end
-
-
#
-
# == Args
-
#
-
# +uri+::
-
# String
-
#
-
# == Description
-
#
-
# Parses +uri+ and constructs either matching URI scheme object
-
# (File, FTP, HTTP, HTTPS, LDAP, LDAPS, or MailTo) or URI::Generic.
-
#
-
# == Usage
-
#
-
# p = URI::Parser.new
-
# p.parse("ldap://ldap.example.com/dc=example?user=john")
-
# #=> #<URI::LDAP ldap://ldap.example.com/dc=example?user=john>
-
#
-
1
def parse(uri)
-
scheme, userinfo, host, port,
-
registry, path, opaque, query, fragment = self.split(uri)
-
-
if scheme && URI.scheme_list.include?(scheme.upcase)
-
URI.scheme_list[scheme.upcase].new(scheme, userinfo, host, port,
-
registry, path, opaque, query,
-
fragment, self)
-
else
-
Generic.new(scheme, userinfo, host, port,
-
registry, path, opaque, query,
-
fragment, self)
-
end
-
end
-
-
-
#
-
# == Args
-
#
-
# +uris+::
-
# an Array of Strings
-
#
-
# == Description
-
#
-
# Attempts to parse and merge a set of URIs.
-
#
-
1
def join(*uris)
-
uris[0] = convert_to_uri(uris[0])
-
uris.inject :merge
-
end
-
-
#
-
# :call-seq:
-
# extract( str )
-
# extract( str, schemes )
-
# extract( str, schemes ) {|item| block }
-
#
-
# == Args
-
#
-
# +str+::
-
# String to search
-
# +schemes+::
-
# Patterns to apply to +str+
-
#
-
# == Description
-
#
-
# Attempts to parse and merge a set of URIs.
-
# If no +block+ given, then returns the result,
-
# else it calls +block+ for each element in result.
-
#
-
# See also URI::Parser.make_regexp.
-
#
-
1
def extract(str, schemes = nil)
-
if block_given?
-
str.scan(make_regexp(schemes)) { yield $& }
-
nil
-
else
-
result = []
-
str.scan(make_regexp(schemes)) { result.push $& }
-
result
-
end
-
end
-
-
# Returns Regexp that is default self.regexp[:ABS_URI_REF],
-
# unless +schemes+ is provided. Then it is a Regexp.union with self.pattern[:X_ABS_URI].
-
1
def make_regexp(schemes = nil)
-
unless schemes
-
@regexp[:ABS_URI_REF]
-
else
-
/(?=#{Regexp.union(*schemes)}:)#{@pattern[:X_ABS_URI]}/x
-
end
-
end
-
-
#
-
# :call-seq:
-
# escape( str )
-
# escape( str, unsafe )
-
#
-
# == Args
-
#
-
# +str+::
-
# String to make safe
-
# +unsafe+::
-
# Regexp to apply. Defaults to self.regexp[:UNSAFE]
-
#
-
# == Description
-
#
-
# Constructs a safe String from +str+, removing unsafe characters,
-
# replacing them with codes.
-
#
-
1
def escape(str, unsafe = @regexp[:UNSAFE])
-
unless unsafe.kind_of?(Regexp)
-
# perhaps unsafe is String object
-
unsafe = Regexp.new("[#{Regexp.quote(unsafe)}]", false)
-
end
-
str.gsub(unsafe) do
-
us = $&
-
tmp = ''
-
us.each_byte do |uc|
-
tmp << sprintf('%%%02X', uc)
-
end
-
tmp
-
end.force_encoding(Encoding::US_ASCII)
-
end
-
-
#
-
# :call-seq:
-
# unescape( str )
-
# unescape( str, escaped )
-
#
-
# == Args
-
#
-
# +str+::
-
# String to remove escapes from
-
# +escaped+::
-
# Regexp to apply. Defaults to self.regexp[:ESCAPED]
-
#
-
# == Description
-
#
-
# Removes escapes from +str+.
-
#
-
1
def unescape(str, escaped = @regexp[:ESCAPED])
-
enc = str.encoding
-
enc = Encoding::UTF_8 if enc == Encoding::US_ASCII
-
str.gsub(escaped) { [$&[1, 2]].pack('H2').force_encoding(enc) }
-
end
-
-
1
@@to_s = Kernel.instance_method(:to_s)
-
1
def inspect
-
@@to_s.bind_call(self)
-
end
-
-
1
private
-
-
# Constructs the default Hash of patterns.
-
1
def initialize_pattern(opts = {})
-
1
ret = {}
-
1
ret[:ESCAPED] = escaped = (opts.delete(:ESCAPED) || PATTERN::ESCAPED)
-
1
ret[:UNRESERVED] = unreserved = opts.delete(:UNRESERVED) || PATTERN::UNRESERVED
-
1
ret[:RESERVED] = reserved = opts.delete(:RESERVED) || PATTERN::RESERVED
-
1
ret[:DOMLABEL] = opts.delete(:DOMLABEL) || PATTERN::DOMLABEL
-
1
ret[:TOPLABEL] = opts.delete(:TOPLABEL) || PATTERN::TOPLABEL
-
1
ret[:HOSTNAME] = hostname = opts.delete(:HOSTNAME)
-
-
# RFC 2396 (URI Generic Syntax)
-
# RFC 2732 (IPv6 Literal Addresses in URL's)
-
# RFC 2373 (IPv6 Addressing Architecture)
-
-
# uric = reserved | unreserved | escaped
-
1
ret[:URIC] = uric = "(?:[#{unreserved}#{reserved}]|#{escaped})"
-
# uric_no_slash = unreserved | escaped | ";" | "?" | ":" | "@" |
-
# "&" | "=" | "+" | "$" | ","
-
1
ret[:URIC_NO_SLASH] = uric_no_slash = "(?:[#{unreserved};?:@&=+$,]|#{escaped})"
-
# query = *uric
-
1
ret[:QUERY] = query = "#{uric}*"
-
# fragment = *uric
-
1
ret[:FRAGMENT] = fragment = "#{uric}*"
-
-
# hostname = *( domainlabel "." ) toplabel [ "." ]
-
# reg-name = *( unreserved / pct-encoded / sub-delims ) # RFC3986
-
1
unless hostname
-
1
ret[:HOSTNAME] = hostname = "(?:[a-zA-Z0-9\\-.]|%\\h\\h)+"
-
end
-
-
# RFC 2373, APPENDIX B:
-
# IPv6address = hexpart [ ":" IPv4address ]
-
# IPv4address = 1*3DIGIT "." 1*3DIGIT "." 1*3DIGIT "." 1*3DIGIT
-
# hexpart = hexseq | hexseq "::" [ hexseq ] | "::" [ hexseq ]
-
# hexseq = hex4 *( ":" hex4)
-
# hex4 = 1*4HEXDIG
-
#
-
# XXX: This definition has a flaw. "::" + IPv4address must be
-
# allowed too. Here is a replacement.
-
#
-
# IPv4address = 1*3DIGIT "." 1*3DIGIT "." 1*3DIGIT "." 1*3DIGIT
-
1
ret[:IPV4ADDR] = ipv4addr = "\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}"
-
# hex4 = 1*4HEXDIG
-
1
hex4 = "[#{PATTERN::HEX}]{1,4}"
-
# lastpart = hex4 | IPv4address
-
1
lastpart = "(?:#{hex4}|#{ipv4addr})"
-
# hexseq1 = *( hex4 ":" ) hex4
-
1
hexseq1 = "(?:#{hex4}:)*#{hex4}"
-
# hexseq2 = *( hex4 ":" ) lastpart
-
1
hexseq2 = "(?:#{hex4}:)*#{lastpart}"
-
# IPv6address = hexseq2 | [ hexseq1 ] "::" [ hexseq2 ]
-
1
ret[:IPV6ADDR] = ipv6addr = "(?:#{hexseq2}|(?:#{hexseq1})?::(?:#{hexseq2})?)"
-
-
# IPv6prefix = ( hexseq1 | [ hexseq1 ] "::" [ hexseq1 ] ) "/" 1*2DIGIT
-
# unused
-
-
# ipv6reference = "[" IPv6address "]" (RFC 2732)
-
1
ret[:IPV6REF] = ipv6ref = "\\[#{ipv6addr}\\]"
-
-
# host = hostname | IPv4address
-
# host = hostname | IPv4address | IPv6reference (RFC 2732)
-
1
ret[:HOST] = host = "(?:#{hostname}|#{ipv4addr}|#{ipv6ref})"
-
# port = *digit
-
1
ret[:PORT] = port = '\d*'
-
# hostport = host [ ":" port ]
-
1
ret[:HOSTPORT] = hostport = "#{host}(?::#{port})?"
-
-
# userinfo = *( unreserved | escaped |
-
# ";" | ":" | "&" | "=" | "+" | "$" | "," )
-
1
ret[:USERINFO] = userinfo = "(?:[#{unreserved};:&=+$,]|#{escaped})*"
-
-
# pchar = unreserved | escaped |
-
# ":" | "@" | "&" | "=" | "+" | "$" | ","
-
1
pchar = "(?:[#{unreserved}:@&=+$,]|#{escaped})"
-
# param = *pchar
-
1
param = "#{pchar}*"
-
# segment = *pchar *( ";" param )
-
1
segment = "#{pchar}*(?:;#{param})*"
-
# path_segments = segment *( "/" segment )
-
1
ret[:PATH_SEGMENTS] = path_segments = "#{segment}(?:/#{segment})*"
-
-
# server = [ [ userinfo "@" ] hostport ]
-
1
server = "(?:#{userinfo}@)?#{hostport}"
-
# reg_name = 1*( unreserved | escaped | "$" | "," |
-
# ";" | ":" | "@" | "&" | "=" | "+" )
-
1
ret[:REG_NAME] = reg_name = "(?:[#{unreserved}$,;:@&=+]|#{escaped})+"
-
# authority = server | reg_name
-
1
authority = "(?:#{server}|#{reg_name})"
-
-
# rel_segment = 1*( unreserved | escaped |
-
# ";" | "@" | "&" | "=" | "+" | "$" | "," )
-
1
ret[:REL_SEGMENT] = rel_segment = "(?:[#{unreserved};@&=+$,]|#{escaped})+"
-
-
# scheme = alpha *( alpha | digit | "+" | "-" | "." )
-
1
ret[:SCHEME] = scheme = "[#{PATTERN::ALPHA}][\\-+.#{PATTERN::ALPHA}\\d]*"
-
-
# abs_path = "/" path_segments
-
1
ret[:ABS_PATH] = abs_path = "/#{path_segments}"
-
# rel_path = rel_segment [ abs_path ]
-
1
ret[:REL_PATH] = rel_path = "#{rel_segment}(?:#{abs_path})?"
-
# net_path = "//" authority [ abs_path ]
-
1
ret[:NET_PATH] = net_path = "//#{authority}(?:#{abs_path})?"
-
-
# hier_part = ( net_path | abs_path ) [ "?" query ]
-
1
ret[:HIER_PART] = hier_part = "(?:#{net_path}|#{abs_path})(?:\\?(?:#{query}))?"
-
# opaque_part = uric_no_slash *uric
-
1
ret[:OPAQUE_PART] = opaque_part = "#{uric_no_slash}#{uric}*"
-
-
# absoluteURI = scheme ":" ( hier_part | opaque_part )
-
1
ret[:ABS_URI] = abs_uri = "#{scheme}:(?:#{hier_part}|#{opaque_part})"
-
# relativeURI = ( net_path | abs_path | rel_path ) [ "?" query ]
-
1
ret[:REL_URI] = rel_uri = "(?:#{net_path}|#{abs_path}|#{rel_path})(?:\\?#{query})?"
-
-
# URI-reference = [ absoluteURI | relativeURI ] [ "#" fragment ]
-
1
ret[:URI_REF] = "(?:#{abs_uri}|#{rel_uri})?(?:##{fragment})?"
-
-
1
ret[:X_ABS_URI] = "
-
(#{scheme}): (?# 1: scheme)
-
(?:
-
(#{opaque_part}) (?# 2: opaque)
-
|
-
(?:(?:
-
//(?:
-
(?:(?:(#{userinfo})@)? (?# 3: userinfo)
-
(?:(#{host})(?::(\\d*))?))? (?# 4: host, 5: port)
-
|
-
(#{reg_name}) (?# 6: registry)
-
)
-
|
-
(?!//)) (?# XXX: '//' is the mark for hostport)
-
(#{abs_path})? (?# 7: path)
-
)(?:\\?(#{query}))? (?# 8: query)
-
)
-
(?:\\#(#{fragment}))? (?# 9: fragment)
-
"
-
-
1
ret[:X_REL_URI] = "
-
(?:
-
(?:
-
//
-
(?:
-
(?:(#{userinfo})@)? (?# 1: userinfo)
-
(#{host})?(?::(\\d*))? (?# 2: host, 3: port)
-
|
-
(#{reg_name}) (?# 4: registry)
-
)
-
)
-
|
-
(#{rel_segment}) (?# 5: rel_segment)
-
)?
-
(#{abs_path})? (?# 6: abs_path)
-
(?:\\?(#{query}))? (?# 7: query)
-
(?:\\#(#{fragment}))? (?# 8: fragment)
-
"
-
-
1
ret
-
end
-
-
# Constructs the default Hash of Regexp's.
-
1
def initialize_regexp(pattern)
-
1
ret = {}
-
-
# for URI::split
-
1
ret[:ABS_URI] = Regexp.new('\A\s*' + pattern[:X_ABS_URI] + '\s*\z', Regexp::EXTENDED)
-
1
ret[:REL_URI] = Regexp.new('\A\s*' + pattern[:X_REL_URI] + '\s*\z', Regexp::EXTENDED)
-
-
# for URI::extract
-
1
ret[:URI_REF] = Regexp.new(pattern[:URI_REF])
-
1
ret[:ABS_URI_REF] = Regexp.new(pattern[:X_ABS_URI], Regexp::EXTENDED)
-
1
ret[:REL_URI_REF] = Regexp.new(pattern[:X_REL_URI], Regexp::EXTENDED)
-
-
# for URI::escape/unescape
-
1
ret[:ESCAPED] = Regexp.new(pattern[:ESCAPED])
-
1
ret[:UNSAFE] = Regexp.new("[^#{pattern[:UNRESERVED]}#{pattern[:RESERVED]}]")
-
-
# for Generic#initialize
-
1
ret[:SCHEME] = Regexp.new("\\A#{pattern[:SCHEME]}\\z")
-
1
ret[:USERINFO] = Regexp.new("\\A#{pattern[:USERINFO]}\\z")
-
1
ret[:HOST] = Regexp.new("\\A#{pattern[:HOST]}\\z")
-
1
ret[:PORT] = Regexp.new("\\A#{pattern[:PORT]}\\z")
-
1
ret[:OPAQUE] = Regexp.new("\\A#{pattern[:OPAQUE_PART]}\\z")
-
1
ret[:REGISTRY] = Regexp.new("\\A#{pattern[:REG_NAME]}\\z")
-
1
ret[:ABS_PATH] = Regexp.new("\\A#{pattern[:ABS_PATH]}\\z")
-
1
ret[:REL_PATH] = Regexp.new("\\A#{pattern[:REL_PATH]}\\z")
-
1
ret[:QUERY] = Regexp.new("\\A#{pattern[:QUERY]}\\z")
-
1
ret[:FRAGMENT] = Regexp.new("\\A#{pattern[:FRAGMENT]}\\z")
-
-
1
ret
-
end
-
-
1
def convert_to_uri(uri)
-
if uri.is_a?(URI::Generic)
-
uri
-
elsif uri = String.try_convert(uri)
-
parse(uri)
-
else
-
raise ArgumentError,
-
"bad argument (expected URI object or URI string)"
-
end
-
end
-
-
end # class Parser
-
end # module URI
-
# frozen_string_literal: false
-
1
module URI
-
1
class RFC3986_Parser # :nodoc:
-
# URI defined in RFC3986
-
# this regexp is modified not to host is not empty string
-
1
RFC3986_URI = /\A(?<URI>(?<scheme>[A-Za-z][+\-.0-9A-Za-z]*):(?<hier-part>\/\/(?<authority>(?:(?<userinfo>(?:%\h\h|[!$&-.0-;=A-Z_a-z~])*)@)?(?<host>(?<IP-literal>\[(?:(?<IPv6address>(?:\h{1,4}:){6}(?<ls32>\h{1,4}:\h{1,4}|(?<IPv4address>(?<dec-octet>[1-9]\d|1\d{2}|2[0-4]\d|25[0-5]|\d)\.\g<dec-octet>\.\g<dec-octet>\.\g<dec-octet>))|::(?:\h{1,4}:){5}\g<ls32>|\h{1,4}?::(?:\h{1,4}:){4}\g<ls32>|(?:(?:\h{1,4}:)?\h{1,4})?::(?:\h{1,4}:){3}\g<ls32>|(?:(?:\h{1,4}:){,2}\h{1,4})?::(?:\h{1,4}:){2}\g<ls32>|(?:(?:\h{1,4}:){,3}\h{1,4})?::\h{1,4}:\g<ls32>|(?:(?:\h{1,4}:){,4}\h{1,4})?::\g<ls32>|(?:(?:\h{1,4}:){,5}\h{1,4})?::\h{1,4}|(?:(?:\h{1,4}:){,6}\h{1,4})?::)|(?<IPvFuture>v\h+\.[!$&-.0-;=A-Z_a-z~]+))\])|\g<IPv4address>|(?<reg-name>(?:%\h\h|[!$&-.0-9;=A-Z_a-z~])+))?(?::(?<port>\d*))?)(?<path-abempty>(?:\/(?<segment>(?:%\h\h|[!$&-.0-;=@-Z_a-z~])*))*)|(?<path-absolute>\/(?:(?<segment-nz>(?:%\h\h|[!$&-.0-;=@-Z_a-z~])+)(?:\/\g<segment>)*)?)|(?<path-rootless>\g<segment-nz>(?:\/\g<segment>)*)|(?<path-empty>))(?:\?(?<query>[^#]*))?(?:\#(?<fragment>(?:%\h\h|[!$&-.0-;=@-Z_a-z~\/?])*))?)\z/
-
1
RFC3986_relative_ref = /\A(?<relative-ref>(?<relative-part>\/\/(?<authority>(?:(?<userinfo>(?:%\h\h|[!$&-.0-;=A-Z_a-z~])*)@)?(?<host>(?<IP-literal>\[(?<IPv6address>(?:\h{1,4}:){6}(?<ls32>\h{1,4}:\h{1,4}|(?<IPv4address>(?<dec-octet>[1-9]\d|1\d{2}|2[0-4]\d|25[0-5]|\d)\.\g<dec-octet>\.\g<dec-octet>\.\g<dec-octet>))|::(?:\h{1,4}:){5}\g<ls32>|\h{1,4}?::(?:\h{1,4}:){4}\g<ls32>|(?:(?:\h{1,4}:){,1}\h{1,4})?::(?:\h{1,4}:){3}\g<ls32>|(?:(?:\h{1,4}:){,2}\h{1,4})?::(?:\h{1,4}:){2}\g<ls32>|(?:(?:\h{1,4}:){,3}\h{1,4})?::\h{1,4}:\g<ls32>|(?:(?:\h{1,4}:){,4}\h{1,4})?::\g<ls32>|(?:(?:\h{1,4}:){,5}\h{1,4})?::\h{1,4}|(?:(?:\h{1,4}:){,6}\h{1,4})?::)|(?<IPvFuture>v\h+\.[!$&-.0-;=A-Z_a-z~]+)\])|\g<IPv4address>|(?<reg-name>(?:%\h\h|[!$&-.0-9;=A-Z_a-z~])+))?(?::(?<port>\d*))?)(?<path-abempty>(?:\/(?<segment>(?:%\h\h|[!$&-.0-;=@-Z_a-z~])*))*)|(?<path-absolute>\/(?:(?<segment-nz>(?:%\h\h|[!$&-.0-;=@-Z_a-z~])+)(?:\/\g<segment>)*)?)|(?<path-noscheme>(?<segment-nz-nc>(?:%\h\h|[!$&-.0-9;=@-Z_a-z~])+)(?:\/\g<segment>)*)|(?<path-empty>))(?:\?(?<query>[^#]*))?(?:\#(?<fragment>(?:%\h\h|[!$&-.0-;=@-Z_a-z~\/?])*))?)\z/
-
1
attr_reader :regexp
-
-
1
def initialize
-
1
@regexp = default_regexp.each_value(&:freeze).freeze
-
end
-
-
1
def split(uri) #:nodoc:
-
begin
-
3
uri = uri.to_str
-
rescue NoMethodError
-
raise InvalidURIError, "bad URI(is not URI?): #{uri.inspect}"
-
end
-
3
uri.ascii_only? or
-
raise InvalidURIError, "URI must be ascii only #{uri.dump}"
-
3
if m = RFC3986_URI.match(uri)
-
3
query = m["query".freeze]
-
3
scheme = m["scheme".freeze]
-
3
opaque = m["path-rootless".freeze]
-
3
if opaque
-
opaque << "?#{query}" if query
-
[ scheme,
-
nil, # userinfo
-
nil, # host
-
nil, # port
-
nil, # registry
-
nil, # path
-
opaque,
-
nil, # query
-
m["fragment".freeze]
-
]
-
else # normal
-
3
[ scheme,
-
m["userinfo".freeze],
-
m["host".freeze],
-
m["port".freeze],
-
nil, # registry
-
3
(m["path-abempty".freeze] ||
-
m["path-absolute".freeze] ||
-
m["path-empty".freeze]),
-
nil, # opaque
-
query,
-
m["fragment".freeze]
-
]
-
end
-
elsif m = RFC3986_relative_ref.match(uri)
-
[ nil, # scheme
-
m["userinfo".freeze],
-
m["host".freeze],
-
m["port".freeze],
-
nil, # registry,
-
(m["path-abempty".freeze] ||
-
m["path-absolute".freeze] ||
-
m["path-noscheme".freeze] ||
-
m["path-empty".freeze]),
-
nil, # opaque
-
m["query".freeze],
-
m["fragment".freeze]
-
]
-
else
-
raise InvalidURIError, "bad URI(is not URI?): #{uri.inspect}"
-
end
-
end
-
-
1
def parse(uri) # :nodoc:
-
scheme, userinfo, host, port,
-
3
registry, path, opaque, query, fragment = self.split(uri)
-
3
scheme_list = URI.scheme_list
-
3
if scheme && scheme_list.include?(uc = scheme.upcase)
-
3
scheme_list[uc].new(scheme, userinfo, host, port,
-
registry, path, opaque, query,
-
fragment, self)
-
else
-
Generic.new(scheme, userinfo, host, port,
-
registry, path, opaque, query,
-
fragment, self)
-
end
-
end
-
-
-
1
def join(*uris) # :nodoc:
-
uris[0] = convert_to_uri(uris[0])
-
uris.inject :merge
-
end
-
-
1
@@to_s = Kernel.instance_method(:to_s)
-
1
def inspect
-
@@to_s.bind_call(self)
-
end
-
-
1
private
-
-
1
def default_regexp # :nodoc:
-
1
{
-
SCHEME: /\A[A-Za-z][A-Za-z0-9+\-.]*\z/,
-
USERINFO: /\A(?:%\h\h|[!$&-.0-;=A-Z_a-z~])*\z/,
-
HOST: /\A(?:(?<IP-literal>\[(?:(?<IPv6address>(?:\h{1,4}:){6}(?<ls32>\h{1,4}:\h{1,4}|(?<IPv4address>(?<dec-octet>[1-9]\d|1\d{2}|2[0-4]\d|25[0-5]|\d)\.\g<dec-octet>\.\g<dec-octet>\.\g<dec-octet>))|::(?:\h{1,4}:){5}\g<ls32>|\h{,4}::(?:\h{1,4}:){4}\g<ls32>|(?:(?:\h{1,4}:)?\h{1,4})?::(?:\h{1,4}:){3}\g<ls32>|(?:(?:\h{1,4}:){,2}\h{1,4})?::(?:\h{1,4}:){2}\g<ls32>|(?:(?:\h{1,4}:){,3}\h{1,4})?::\h{1,4}:\g<ls32>|(?:(?:\h{1,4}:){,4}\h{1,4})?::\g<ls32>|(?:(?:\h{1,4}:){,5}\h{1,4})?::\h{1,4}|(?:(?:\h{1,4}:){,6}\h{1,4})?::)|(?<IPvFuture>v\h+\.[!$&-.0-;=A-Z_a-z~]+))\])|\g<IPv4address>|(?<reg-name>(?:%\h\h|[!$&-.0-9;=A-Z_a-z~])*))\z/,
-
ABS_PATH: /\A\/(?:%\h\h|[!$&-.0-;=@-Z_a-z~])*(?:\/(?:%\h\h|[!$&-.0-;=@-Z_a-z~])*)*\z/,
-
REL_PATH: /\A(?:%\h\h|[!$&-.0-;=@-Z_a-z~])+(?:\/(?:%\h\h|[!$&-.0-;=@-Z_a-z~])*)*\z/,
-
QUERY: /\A(?:%\h\h|[!$&-.0-;=@-Z_a-z~\/?])*\z/,
-
FRAGMENT: /\A(?:%\h\h|[!$&-.0-;=@-Z_a-z~\/?])*\z/,
-
OPAQUE: /\A(?:[^\/].*)?\z/,
-
PORT: /\A[\x09\x0a\x0c\x0d ]*\d*[\x09\x0a\x0c\x0d ]*\z/,
-
}
-
end
-
-
1
def convert_to_uri(uri)
-
if uri.is_a?(URI::Generic)
-
uri
-
elsif uri = String.try_convert(uri)
-
parse(uri)
-
else
-
raise ArgumentError,
-
"bad argument (expected URI object or URI string)"
-
end
-
end
-
-
end # class Parser
-
end # module URI
-
1
module URI
-
# :stopdoc:
-
1
VERSION_CODE = '001000'.freeze
-
4
VERSION = VERSION_CODE.scan(/../).collect{|n| n.to_i}.join('.').freeze
-
# :startdoc:
-
end
-
1
require 'concurrent/version'
-
1
require 'concurrent/constants'
-
1
require 'concurrent/errors'
-
1
require 'concurrent/configuration'
-
-
1
require 'concurrent/atomics'
-
1
require 'concurrent/executors'
-
1
require 'concurrent/synchronization'
-
-
1
require 'concurrent/atomic/atomic_markable_reference'
-
1
require 'concurrent/atomic/atomic_reference'
-
1
require 'concurrent/agent'
-
1
require 'concurrent/atom'
-
1
require 'concurrent/array'
-
1
require 'concurrent/hash'
-
1
require 'concurrent/set'
-
1
require 'concurrent/map'
-
1
require 'concurrent/tuple'
-
1
require 'concurrent/async'
-
1
require 'concurrent/dataflow'
-
1
require 'concurrent/delay'
-
1
require 'concurrent/exchanger'
-
1
require 'concurrent/future'
-
1
require 'concurrent/immutable_struct'
-
1
require 'concurrent/ivar'
-
1
require 'concurrent/maybe'
-
1
require 'concurrent/mutable_struct'
-
1
require 'concurrent/mvar'
-
1
require 'concurrent/promise'
-
1
require 'concurrent/scheduled_task'
-
1
require 'concurrent/settable_struct'
-
1
require 'concurrent/timer_task'
-
1
require 'concurrent/tvar'
-
1
require 'concurrent/promises'
-
-
1
require 'concurrent/thread_safe/synchronized_delegator'
-
1
require 'concurrent/thread_safe/util'
-
-
1
require 'concurrent/options'
-
-
# @!macro internal_implementation_note
-
#
-
# @note **Private Implementation:** This abstraction is a private, internal
-
# implementation detail. It should never be used directly.
-
-
# @!macro monotonic_clock_warning
-
#
-
# @note Time calculations on all platforms and languages are sensitive to
-
# changes to the system clock. To alleviate the potential problems
-
# associated with changing the system clock while an application is running,
-
# most modern operating systems provide a monotonic clock that operates
-
# independently of the system clock. A monotonic clock cannot be used to
-
# determine human-friendly clock times. A monotonic clock is used exclusively
-
# for calculating time intervals. Not all Ruby platforms provide access to an
-
# operating system monotonic clock. On these platforms a pure-Ruby monotonic
-
# clock will be used as a fallback. An operating system monotonic clock is both
-
# faster and more reliable than the pure-Ruby implementation. The pure-Ruby
-
# implementation should be fast and reliable enough for most non-realtime
-
# operations. At this time the common Ruby platforms that provide access to an
-
# operating system monotonic clock are MRI 2.1 and above and JRuby (all versions).
-
#
-
# @see http://linux.die.net/man/3/clock_gettime Linux clock_gettime(3)
-
-
# @!macro copy_options
-
#
-
# ## Copy Options
-
#
-
# Object references in Ruby are mutable. This can lead to serious
-
# problems when the {#value} of an object is a mutable reference. Which
-
# is always the case unless the value is a `Fixnum`, `Symbol`, or similar
-
# "primitive" data type. Each instance can be configured with a few
-
# options that can help protect the program from potentially dangerous
-
# operations. Each of these options can be optionally set when the object
-
# instance is created:
-
#
-
# * `:dup_on_deref` When true the object will call the `#dup` method on
-
# the `value` object every time the `#value` method is called
-
# (default: false)
-
# * `:freeze_on_deref` When true the object will call the `#freeze`
-
# method on the `value` object every time the `#value` method is called
-
# (default: false)
-
# * `:copy_on_deref` When given a `Proc` object the `Proc` will be run
-
# every time the `#value` method is called. The `Proc` will be given
-
# the current `value` as its only argument and the result returned by
-
# the block will be the return value of the `#value` call. When `nil`
-
# this option will be ignored (default: nil)
-
#
-
# When multiple deref options are set the order of operations is strictly defined.
-
# The order of deref operations is:
-
# * `:copy_on_deref`
-
# * `:dup_on_deref`
-
# * `:freeze_on_deref`
-
#
-
# Because of this ordering there is no need to `#freeze` an object created by a
-
# provided `:copy_on_deref` block. Simply set `:freeze_on_deref` to `true`.
-
# Setting both `:dup_on_deref` to `true` and `:freeze_on_deref` to `true` is
-
# as close to the behavior of a "pure" functional language (like Erlang, Clojure,
-
# or Haskell) as we are likely to get in Ruby.
-
-
# @!macro deref_options
-
#
-
# @option opts [Boolean] :dup_on_deref (false) Call `#dup` before
-
# returning the data from {#value}
-
# @option opts [Boolean] :freeze_on_deref (false) Call `#freeze` before
-
# returning the data from {#value}
-
# @option opts [Proc] :copy_on_deref (nil) When calling the {#value}
-
# method, call the given proc passing the internal value as the sole
-
# argument then return the new value returned from the proc.
-
-
# @!macro executor_and_deref_options
-
#
-
# @param [Hash] opts the options used to define the behavior at update and deref
-
# and to specify the executor on which to perform actions
-
# @option opts [Executor] :executor when set use the given `Executor` instance.
-
# Three special values are also supported: `:io` returns the global pool for
-
# long, blocking (IO) tasks, `:fast` returns the global pool for short, fast
-
# operations, and `:immediate` returns the global `ImmediateExecutor` object.
-
# @!macro deref_options
-
-
# @!macro warn.edge
-
# @api Edge
-
# @note **Edge Features** are under active development and may change frequently.
-
#
-
# - Deprecations are not added before incompatible changes.
-
# - Edge version: _major_ is always 0, _minor_ bump means incompatible change,
-
# _patch_ bump means compatible change.
-
# - Edge features may also lack tests and documentation.
-
# - Features developed in `concurrent-ruby-edge` are expected to move
-
# to `concurrent-ruby` when finalised.
-
-
-
# {include:file:README.md}
-
1
module Concurrent
-
end
-
1
require 'concurrent/configuration'
-
1
require 'concurrent/atomic/atomic_reference'
-
1
require 'concurrent/atomic/thread_local_var'
-
1
require 'concurrent/collection/copy_on_write_observer_set'
-
1
require 'concurrent/concern/observable'
-
1
require 'concurrent/synchronization'
-
-
1
module Concurrent
-
-
# `Agent` is inspired by Clojure's [agent](http://clojure.org/agents)
-
# function. An agent is a shared, mutable variable providing independent,
-
# uncoordinated, *asynchronous* change of individual values. Best used when
-
# the value will undergo frequent, complex updates. Suitable when the result
-
# of an update does not need to be known immediately. `Agent` is (mostly)
-
# functionally equivalent to Clojure's agent, except where the runtime
-
# prevents parity.
-
#
-
# Agents are reactive, not autonomous - there is no imperative message loop
-
# and no blocking receive. The state of an Agent should be itself immutable
-
# and the `#value` of an Agent is always immediately available for reading by
-
# any thread without any messages, i.e. observation does not require
-
# cooperation or coordination.
-
#
-
# Agent action dispatches are made using the various `#send` methods. These
-
# methods always return immediately. At some point later, in another thread,
-
# the following will happen:
-
#
-
# 1. The given `action` will be applied to the state of the Agent and the
-
# `args`, if any were supplied.
-
# 2. The return value of `action` will be passed to the validator lambda,
-
# if one has been set on the Agent.
-
# 3. If the validator succeeds or if no validator was given, the return value
-
# of the given `action` will become the new `#value` of the Agent. See
-
# `#initialize` for details.
-
# 4. If any observers were added to the Agent, they will be notified. See
-
# `#add_observer` for details.
-
# 5. If during the `action` execution any other dispatches are made (directly
-
# or indirectly), they will be held until after the `#value` of the Agent
-
# has been changed.
-
#
-
# If any exceptions are thrown by an action function, no nested dispatches
-
# will occur, and the exception will be cached in the Agent itself. When an
-
# Agent has errors cached, any subsequent interactions will immediately throw
-
# an exception, until the agent's errors are cleared. Agent errors can be
-
# examined with `#error` and the agent restarted with `#restart`.
-
#
-
# The actions of all Agents get interleaved amongst threads in a thread pool.
-
# At any point in time, at most one action for each Agent is being executed.
-
# Actions dispatched to an agent from another single agent or thread will
-
# occur in the order they were sent, potentially interleaved with actions
-
# dispatched to the same agent from other sources. The `#send` method should
-
# be used for actions that are CPU limited, while the `#send_off` method is
-
# appropriate for actions that may block on IO.
-
#
-
# Unlike in Clojure, `Agent` cannot participate in `Concurrent::TVar` transactions.
-
#
-
# ## Example
-
#
-
# ```
-
# def next_fibonacci(set = nil)
-
# return [0, 1] if set.nil?
-
# set + [set[-2..-1].reduce{|sum,x| sum + x }]
-
# end
-
#
-
# # create an agent with an initial value
-
# agent = Concurrent::Agent.new(next_fibonacci)
-
#
-
# # send a few update requests
-
# 5.times do
-
# agent.send{|set| next_fibonacci(set) }
-
# end
-
#
-
# # wait for them to complete
-
# agent.await
-
#
-
# # get the current value
-
# agent.value #=> [0, 1, 1, 2, 3, 5, 8]
-
# ```
-
#
-
# ## Observation
-
#
-
# Agents support observers through the {Concurrent::Observable} mixin module.
-
# Notification of observers occurs every time an action dispatch returns and
-
# the new value is successfully validated. Observation will *not* occur if the
-
# action raises an exception, if validation fails, or when a {#restart} occurs.
-
#
-
# When notified the observer will receive three arguments: `time`, `old_value`,
-
# and `new_value`. The `time` argument is the time at which the value change
-
# occurred. The `old_value` is the value of the Agent when the action began
-
# processing. The `new_value` is the value to which the Agent was set when the
-
# action completed. Note that `old_value` and `new_value` may be the same.
-
# This is not an error. It simply means that the action returned the same
-
# value.
-
#
-
# ## Nested Actions
-
#
-
# It is possible for an Agent action to post further actions back to itself.
-
# The nested actions will be enqueued normally then processed *after* the
-
# outer action completes, in the order they were sent, possibly interleaved
-
# with action dispatches from other threads. Nested actions never deadlock
-
# with one another and a failure in a nested action will never affect the
-
# outer action.
-
#
-
# Nested actions can be called using the Agent reference from the enclosing
-
# scope or by passing the reference in as a "send" argument. Nested actions
-
# cannot be post using `self` from within the action block/proc/lambda; `self`
-
# in this context will not reference the Agent. The preferred method for
-
# dispatching nested actions is to pass the Agent as an argument. This allows
-
# Ruby to more effectively manage the closing scope.
-
#
-
# Prefer this:
-
#
-
# ```
-
# agent = Concurrent::Agent.new(0)
-
# agent.send(agent) do |value, this|
-
# this.send {|v| v + 42 }
-
# 3.14
-
# end
-
# agent.value #=> 45.14
-
# ```
-
#
-
# Over this:
-
#
-
# ```
-
# agent = Concurrent::Agent.new(0)
-
# agent.send do |value|
-
# agent.send {|v| v + 42 }
-
# 3.14
-
# end
-
# ```
-
#
-
# @!macro agent_await_warning
-
#
-
# **NOTE** Never, *under any circumstances*, call any of the "await" methods
-
# ({#await}, {#await_for}, {#await_for!}, and {#wait}) from within an action
-
# block/proc/lambda. The call will block the Agent and will always fail.
-
# Calling either {#await} or {#wait} (with a timeout of `nil`) will
-
# hopelessly deadlock the Agent with no possibility of recovery.
-
#
-
# @!macro thread_safe_variable_comparison
-
#
-
# @see http://clojure.org/Agents Clojure Agents
-
# @see http://clojure.org/state Values and Change - Clojure's approach to Identity and State
-
1
class Agent < Synchronization::LockableObject
-
1
include Concern::Observable
-
-
1
ERROR_MODES = [:continue, :fail].freeze
-
1
private_constant :ERROR_MODES
-
-
1
AWAIT_FLAG = ::Object.new
-
1
private_constant :AWAIT_FLAG
-
-
1
AWAIT_ACTION = ->(value, latch) { latch.count_down; AWAIT_FLAG }
-
1
private_constant :AWAIT_ACTION
-
-
1
DEFAULT_ERROR_HANDLER = ->(agent, error) { nil }
-
1
private_constant :DEFAULT_ERROR_HANDLER
-
-
1
DEFAULT_VALIDATOR = ->(value) { true }
-
1
private_constant :DEFAULT_VALIDATOR
-
-
1
Job = Struct.new(:action, :args, :executor, :caller)
-
1
private_constant :Job
-
-
# Raised during action processing or any other time in an Agent's lifecycle.
-
1
class Error < StandardError
-
1
def initialize(message = nil)
-
message ||= 'agent must be restarted before jobs can post'
-
super(message)
-
end
-
end
-
-
# Raised when a new value obtained during action processing or at `#restart`
-
# fails validation.
-
1
class ValidationError < Error
-
1
def initialize(message = nil)
-
message ||= 'invalid value'
-
super(message)
-
end
-
end
-
-
# The error mode this Agent is operating in. See {#initialize} for details.
-
1
attr_reader :error_mode
-
-
# Create a new `Agent` with the given initial value and options.
-
#
-
# The `:validator` option must be `nil` or a side-effect free proc/lambda
-
# which takes one argument. On any intended value change the validator, if
-
# provided, will be called. If the new value is invalid the validator should
-
# return `false` or raise an error.
-
#
-
# The `:error_handler` option must be `nil` or a proc/lambda which takes two
-
# arguments. When an action raises an error or validation fails, either by
-
# returning false or raising an error, the error handler will be called. The
-
# arguments to the error handler will be a reference to the agent itself and
-
# the error object which was raised.
-
#
-
# The `:error_mode` may be either `:continue` (the default if an error
-
# handler is given) or `:fail` (the default if error handler nil or not
-
# given).
-
#
-
# If an action being run by the agent throws an error or doesn't pass
-
# validation the error handler, if present, will be called. After the
-
# handler executes if the error mode is `:continue` the Agent will continue
-
# as if neither the action that caused the error nor the error itself ever
-
# happened.
-
#
-
# If the mode is `:fail` the Agent will become {#failed?} and will stop
-
# accepting new action dispatches. Any previously queued actions will be
-
# held until {#restart} is called. The {#value} method will still work,
-
# returning the value of the Agent before the error.
-
#
-
# @param [Object] initial the initial value
-
# @param [Hash] opts the configuration options
-
#
-
# @option opts [Symbol] :error_mode either `:continue` or `:fail`
-
# @option opts [nil, Proc] :error_handler the (optional) error handler
-
# @option opts [nil, Proc] :validator the (optional) validation procedure
-
1
def initialize(initial, opts = {})
-
super()
-
synchronize { ns_initialize(initial, opts) }
-
end
-
-
# The current value (state) of the Agent, irrespective of any pending or
-
# in-progress actions. The value is always available and is non-blocking.
-
#
-
# @return [Object] the current value
-
1
def value
-
@current.value # TODO (pitr 12-Sep-2015): broken unsafe read?
-
end
-
-
1
alias_method :deref, :value
-
-
# When {#failed?} and {#error_mode} is `:fail`, returns the error object
-
# which caused the failure, else `nil`. When {#error_mode} is `:continue`
-
# will *always* return `nil`.
-
#
-
# @return [nil, Error] the error which caused the failure when {#failed?}
-
1
def error
-
@error.value
-
end
-
-
1
alias_method :reason, :error
-
-
# @!macro agent_send
-
#
-
# Dispatches an action to the Agent and returns immediately. Subsequently,
-
# in a thread from a thread pool, the {#value} will be set to the return
-
# value of the action. Action dispatches are only allowed when the Agent
-
# is not {#failed?}.
-
#
-
# The action must be a block/proc/lambda which takes 1 or more arguments.
-
# The first argument is the current {#value} of the Agent. Any arguments
-
# passed to the send method via the `args` parameter will be passed to the
-
# action as the remaining arguments. The action must return the new value
-
# of the Agent.
-
#
-
# * {#send} and {#send!} should be used for actions that are CPU limited
-
# * {#send_off}, {#send_off!}, and {#<<} are appropriate for actions that
-
# may block on IO
-
# * {#send_via} and {#send_via!} are used when a specific executor is to
-
# be used for the action
-
#
-
# @param [Array<Object>] args zero or more arguments to be passed to
-
# the action
-
# @param [Proc] action the action dispatch to be enqueued
-
#
-
# @yield [agent, value, *args] process the old value and return the new
-
# @yieldparam [Object] value the current {#value} of the Agent
-
# @yieldparam [Array<Object>] args zero or more arguments to pass to the
-
# action
-
# @yieldreturn [Object] the new value of the Agent
-
#
-
# @!macro send_return
-
# @return [Boolean] true if the action is successfully enqueued, false if
-
# the Agent is {#failed?}
-
1
def send(*args, &action)
-
enqueue_action_job(action, args, Concurrent.global_fast_executor)
-
end
-
-
# @!macro agent_send
-
#
-
# @!macro send_bang_return_and_raise
-
# @return [Boolean] true if the action is successfully enqueued
-
# @raise [Concurrent::Agent::Error] if the Agent is {#failed?}
-
1
def send!(*args, &action)
-
raise Error.new unless send(*args, &action)
-
true
-
end
-
-
# @!macro agent_send
-
# @!macro send_return
-
1
def send_off(*args, &action)
-
enqueue_action_job(action, args, Concurrent.global_io_executor)
-
end
-
-
1
alias_method :post, :send_off
-
-
# @!macro agent_send
-
# @!macro send_bang_return_and_raise
-
1
def send_off!(*args, &action)
-
raise Error.new unless send_off(*args, &action)
-
true
-
end
-
-
# @!macro agent_send
-
# @!macro send_return
-
# @param [Concurrent::ExecutorService] executor the executor on which the
-
# action is to be dispatched
-
1
def send_via(executor, *args, &action)
-
enqueue_action_job(action, args, executor)
-
end
-
-
# @!macro agent_send
-
# @!macro send_bang_return_and_raise
-
# @param [Concurrent::ExecutorService] executor the executor on which the
-
# action is to be dispatched
-
1
def send_via!(executor, *args, &action)
-
raise Error.new unless send_via(executor, *args, &action)
-
true
-
end
-
-
# Dispatches an action to the Agent and returns immediately. Subsequently,
-
# in a thread from a thread pool, the {#value} will be set to the return
-
# value of the action. Appropriate for actions that may block on IO.
-
#
-
# @param [Proc] action the action dispatch to be enqueued
-
# @return [Concurrent::Agent] self
-
# @see #send_off
-
1
def <<(action)
-
send_off(&action)
-
self
-
end
-
-
# Blocks the current thread (indefinitely!) until all actions dispatched
-
# thus far, from this thread or nested by the Agent, have occurred. Will
-
# block when {#failed?}. Will never return if a failed Agent is {#restart}
-
# with `:clear_actions` true.
-
#
-
# Returns a reference to `self` to support method chaining:
-
#
-
# ```
-
# current_value = agent.await.value
-
# ```
-
#
-
# @return [Boolean] self
-
#
-
# @!macro agent_await_warning
-
1
def await
-
wait(nil)
-
self
-
end
-
-
# Blocks the current thread until all actions dispatched thus far, from this
-
# thread or nested by the Agent, have occurred, or the timeout (in seconds)
-
# has elapsed.
-
#
-
# @param [Float] timeout the maximum number of seconds to wait
-
# @return [Boolean] true if all actions complete before timeout else false
-
#
-
# @!macro agent_await_warning
-
1
def await_for(timeout)
-
wait(timeout.to_f)
-
end
-
-
# Blocks the current thread until all actions dispatched thus far, from this
-
# thread or nested by the Agent, have occurred, or the timeout (in seconds)
-
# has elapsed.
-
#
-
# @param [Float] timeout the maximum number of seconds to wait
-
# @return [Boolean] true if all actions complete before timeout
-
#
-
# @raise [Concurrent::TimeoutError] when timout is reached
-
#
-
# @!macro agent_await_warning
-
1
def await_for!(timeout)
-
raise Concurrent::TimeoutError unless wait(timeout.to_f)
-
true
-
end
-
-
# Blocks the current thread until all actions dispatched thus far, from this
-
# thread or nested by the Agent, have occurred, or the timeout (in seconds)
-
# has elapsed. Will block indefinitely when timeout is nil or not given.
-
#
-
# Provided mainly for consistency with other classes in this library. Prefer
-
# the various `await` methods instead.
-
#
-
# @param [Float] timeout the maximum number of seconds to wait
-
# @return [Boolean] true if all actions complete before timeout else false
-
#
-
# @!macro agent_await_warning
-
1
def wait(timeout = nil)
-
latch = Concurrent::CountDownLatch.new(1)
-
enqueue_await_job(latch)
-
latch.wait(timeout)
-
end
-
-
# Is the Agent in a failed state?
-
#
-
# @see #restart
-
1
def failed?
-
!@error.value.nil?
-
end
-
-
1
alias_method :stopped?, :failed?
-
-
# When an Agent is {#failed?}, changes the Agent {#value} to `new_value`
-
# then un-fails the Agent so that action dispatches are allowed again. If
-
# the `:clear_actions` option is give and true, any actions queued on the
-
# Agent that were being held while it was failed will be discarded,
-
# otherwise those held actions will proceed. The `new_value` must pass the
-
# validator if any, or `restart` will raise an exception and the Agent will
-
# remain failed with its old {#value} and {#error}. Observers, if any, will
-
# not be notified of the new state.
-
#
-
# @param [Object] new_value the new value for the Agent once restarted
-
# @param [Hash] opts the configuration options
-
# @option opts [Symbol] :clear_actions true if all enqueued but unprocessed
-
# actions should be discarded on restart, else false (default: false)
-
# @return [Boolean] true
-
#
-
# @raise [Concurrent:AgentError] when not failed
-
1
def restart(new_value, opts = {})
-
clear_actions = opts.fetch(:clear_actions, false)
-
synchronize do
-
raise Error.new('agent is not failed') unless failed?
-
raise ValidationError unless ns_validate(new_value)
-
@current.value = new_value
-
@error.value = nil
-
@queue.clear if clear_actions
-
ns_post_next_job unless @queue.empty?
-
end
-
true
-
end
-
-
1
class << self
-
-
# Blocks the current thread (indefinitely!) until all actions dispatched
-
# thus far to all the given Agents, from this thread or nested by the
-
# given Agents, have occurred. Will block when any of the agents are
-
# failed. Will never return if a failed Agent is restart with
-
# `:clear_actions` true.
-
#
-
# @param [Array<Concurrent::Agent>] agents the Agents on which to wait
-
# @return [Boolean] true
-
#
-
# @!macro agent_await_warning
-
1
def await(*agents)
-
agents.each { |agent| agent.await }
-
true
-
end
-
-
# Blocks the current thread until all actions dispatched thus far to all
-
# the given Agents, from this thread or nested by the given Agents, have
-
# occurred, or the timeout (in seconds) has elapsed.
-
#
-
# @param [Float] timeout the maximum number of seconds to wait
-
# @param [Array<Concurrent::Agent>] agents the Agents on which to wait
-
# @return [Boolean] true if all actions complete before timeout else false
-
#
-
# @!macro agent_await_warning
-
1
def await_for(timeout, *agents)
-
end_at = Concurrent.monotonic_time + timeout.to_f
-
ok = agents.length.times do |i|
-
break false if (delay = end_at - Concurrent.monotonic_time) < 0
-
break false unless agents[i].await_for(delay)
-
end
-
!!ok
-
end
-
-
# Blocks the current thread until all actions dispatched thus far to all
-
# the given Agents, from this thread or nested by the given Agents, have
-
# occurred, or the timeout (in seconds) has elapsed.
-
#
-
# @param [Float] timeout the maximum number of seconds to wait
-
# @param [Array<Concurrent::Agent>] agents the Agents on which to wait
-
# @return [Boolean] true if all actions complete before timeout
-
#
-
# @raise [Concurrent::TimeoutError] when timout is reached
-
# @!macro agent_await_warning
-
1
def await_for!(timeout, *agents)
-
raise Concurrent::TimeoutError unless await_for(timeout, *agents)
-
true
-
end
-
end
-
-
1
private
-
-
1
def ns_initialize(initial, opts)
-
@error_mode = opts[:error_mode]
-
@error_handler = opts[:error_handler]
-
-
if @error_mode && !ERROR_MODES.include?(@error_mode)
-
raise ArgumentError.new('unrecognized error mode')
-
elsif @error_mode.nil?
-
@error_mode = @error_handler ? :continue : :fail
-
end
-
-
@error_handler ||= DEFAULT_ERROR_HANDLER
-
@validator = opts.fetch(:validator, DEFAULT_VALIDATOR)
-
@current = Concurrent::AtomicReference.new(initial)
-
@error = Concurrent::AtomicReference.new(nil)
-
@caller = Concurrent::ThreadLocalVar.new(nil)
-
@queue = []
-
-
self.observers = Collection::CopyOnNotifyObserverSet.new
-
end
-
-
1
def enqueue_action_job(action, args, executor)
-
raise ArgumentError.new('no action given') unless action
-
job = Job.new(action, args, executor, @caller.value || Thread.current.object_id)
-
synchronize { ns_enqueue_job(job) }
-
end
-
-
1
def enqueue_await_job(latch)
-
synchronize do
-
if (index = ns_find_last_job_for_thread)
-
job = Job.new(AWAIT_ACTION, [latch], Concurrent.global_immediate_executor,
-
Thread.current.object_id)
-
ns_enqueue_job(job, index+1)
-
else
-
latch.count_down
-
true
-
end
-
end
-
end
-
-
1
def ns_enqueue_job(job, index = nil)
-
# a non-nil index means this is an await job
-
return false if index.nil? && failed?
-
index ||= @queue.length
-
@queue.insert(index, job)
-
# if this is the only job, post to executor
-
ns_post_next_job if @queue.length == 1
-
true
-
end
-
-
1
def ns_post_next_job
-
@queue.first.executor.post { execute_next_job }
-
end
-
-
1
def execute_next_job
-
job = synchronize { @queue.first }
-
old_value = @current.value
-
-
@caller.value = job.caller # for nested actions
-
new_value = job.action.call(old_value, *job.args)
-
@caller.value = nil
-
-
return if new_value == AWAIT_FLAG
-
-
if ns_validate(new_value)
-
@current.value = new_value
-
observers.notify_observers(Time.now, old_value, new_value)
-
else
-
handle_error(ValidationError.new)
-
end
-
rescue => error
-
handle_error(error)
-
ensure
-
synchronize do
-
@queue.shift
-
unless failed? || @queue.empty?
-
ns_post_next_job
-
end
-
end
-
end
-
-
1
def ns_validate(value)
-
@validator.call(value)
-
rescue
-
false
-
end
-
-
1
def handle_error(error)
-
# stop new jobs from posting
-
@error.value = error if @error_mode == :fail
-
@error_handler.call(self, error)
-
rescue
-
# do nothing
-
end
-
-
1
def ns_find_last_job_for_thread
-
@queue.rindex { |job| job.caller == Thread.current.object_id }
-
end
-
end
-
end
-
1
require 'concurrent/utility/engine'
-
1
require 'concurrent/thread_safe/util'
-
-
1
module Concurrent
-
-
# @!macro concurrent_array
-
#
-
# A thread-safe subclass of Array. This version locks against the object
-
# itself for every method call, ensuring only one thread can be reading
-
# or writing at a time. This includes iteration methods like `#each`.
-
#
-
# @note `a += b` is **not** a **thread-safe** operation on
-
# `Concurrent::Array`. It reads array `a`, then it creates new `Concurrent::Array`
-
# which is concatenation of `a` and `b`, then it writes the concatenation to `a`.
-
# The read and write are independent operations they do not form a single atomic
-
# operation therefore when two `+=` operations are executed concurrently updates
-
# may be lost. Use `#concat` instead.
-
#
-
# @see http://ruby-doc.org/core-2.2.0/Array.html Ruby standard library `Array`
-
-
# @!macro internal_implementation_note
-
ArrayImplementation = case
-
1
when Concurrent.on_cruby?
-
# Array is thread-safe in practice because CRuby runs
-
# threads one at a time and does not do context
-
# switching during the execution of C functions.
-
1
::Array
-
-
when Concurrent.on_jruby?
-
require 'jruby/synchronized'
-
-
class JRubyArray < ::Array
-
include JRuby::Synchronized
-
end
-
JRubyArray
-
-
when Concurrent.on_rbx?
-
require 'monitor'
-
require 'concurrent/thread_safe/util/data_structures'
-
-
class RbxArray < ::Array
-
end
-
-
ThreadSafe::Util.make_synchronized_on_rbx RbxArray
-
RbxArray
-
-
when Concurrent.on_truffleruby?
-
require 'concurrent/thread_safe/util/data_structures'
-
-
class TruffleRubyArray < ::Array
-
end
-
-
ThreadSafe::Util.make_synchronized_on_truffleruby TruffleRubyArray
-
TruffleRubyArray
-
-
else
-
warn 'Possibly unsupported Ruby implementation'
-
::Array
-
end
-
1
private_constant :ArrayImplementation
-
-
# @!macro concurrent_array
-
1
class Array < ArrayImplementation
-
end
-
-
end
-
1
require 'concurrent/configuration'
-
1
require 'concurrent/ivar'
-
1
require 'concurrent/synchronization/lockable_object'
-
-
1
module Concurrent
-
-
# A mixin module that provides simple asynchronous behavior to a class,
-
# turning it into a simple actor. Loosely based on Erlang's
-
# [gen_server](http://www.erlang.org/doc/man/gen_server.html), but without
-
# supervision or linking.
-
#
-
# A more feature-rich {Concurrent::Actor} is also available when the
-
# capabilities of `Async` are too limited.
-
#
-
# ```cucumber
-
# Feature:
-
# As a stateful, plain old Ruby class
-
# I want safe, asynchronous behavior
-
# So my long-running methods don't block the main thread
-
# ```
-
#
-
# The `Async` module is a way to mix simple yet powerful asynchronous
-
# capabilities into any plain old Ruby object or class, turning each object
-
# into a simple Actor. Method calls are processed on a background thread. The
-
# caller is free to perform other actions while processing occurs in the
-
# background.
-
#
-
# Method calls to the asynchronous object are made via two proxy methods:
-
# `async` (alias `cast`) and `await` (alias `call`). These proxy methods post
-
# the method call to the object's background thread and return a "future"
-
# which will eventually contain the result of the method call.
-
#
-
# This behavior is loosely patterned after Erlang's `gen_server` behavior.
-
# When an Erlang module implements the `gen_server` behavior it becomes
-
# inherently asynchronous. The `start` or `start_link` function spawns a
-
# process (similar to a thread but much more lightweight and efficient) and
-
# returns the ID of the process. Using the process ID, other processes can
-
# send messages to the `gen_server` via the `cast` and `call` methods. Unlike
-
# Erlang's `gen_server`, however, `Async` classes do not support linking or
-
# supervision trees.
-
#
-
# ## Basic Usage
-
#
-
# When this module is mixed into a class, objects of the class become inherently
-
# asynchronous. Each object gets its own background thread on which to post
-
# asynchronous method calls. Asynchronous method calls are executed in the
-
# background one at a time in the order they are received.
-
#
-
# To create an asynchronous class, simply mix in the `Concurrent::Async` module:
-
#
-
# ```
-
# class Hello
-
# include Concurrent::Async
-
#
-
# def hello(name)
-
# "Hello, #{name}!"
-
# end
-
# end
-
# ```
-
#
-
# When defining a constructor it is critical that the first line be a call to
-
# `super` with no arguments. The `super` method initializes the background
-
# thread and other asynchronous components.
-
#
-
# ```
-
# class BackgroundLogger
-
# include Concurrent::Async
-
#
-
# def initialize(level)
-
# super()
-
# @logger = Logger.new(STDOUT)
-
# @logger.level = level
-
# end
-
#
-
# def info(msg)
-
# @logger.info(msg)
-
# end
-
# end
-
# ```
-
#
-
# Mixing this module into a class provides each object two proxy methods:
-
# `async` and `await`. These methods are thread safe with respect to the
-
# enclosing object. The former proxy allows methods to be called
-
# asynchronously by posting to the object's internal thread. The latter proxy
-
# allows a method to be called synchronously but does so safely with respect
-
# to any pending asynchronous method calls and ensures proper ordering. Both
-
# methods return a {Concurrent::IVar} which can be inspected for the result
-
# of the proxied method call. Calling a method with `async` will return a
-
# `:pending` `IVar` whereas `await` will return a `:complete` `IVar`.
-
#
-
# ```
-
# class Echo
-
# include Concurrent::Async
-
#
-
# def echo(msg)
-
# print "#{msg}\n"
-
# end
-
# end
-
#
-
# horn = Echo.new
-
# horn.echo('zero') # synchronous, not thread-safe
-
# # returns the actual return value of the method
-
#
-
# horn.async.echo('one') # asynchronous, non-blocking, thread-safe
-
# # returns an IVar in the :pending state
-
#
-
# horn.await.echo('two') # synchronous, blocking, thread-safe
-
# # returns an IVar in the :complete state
-
# ```
-
#
-
# ## Let It Fail
-
#
-
# The `async` and `await` proxy methods have built-in error protection based
-
# on Erlang's famous "let it fail" philosophy. Instance methods should not be
-
# programmed defensively. When an exception is raised by a delegated method
-
# the proxy will rescue the exception, expose it to the caller as the `reason`
-
# attribute of the returned future, then process the next method call.
-
#
-
# ## Calling Methods Internally
-
#
-
# External method calls should *always* use the `async` and `await` proxy
-
# methods. When one method calls another method, the `async` proxy should
-
# rarely be used and the `await` proxy should *never* be used.
-
#
-
# When an object calls one of its own methods using the `await` proxy the
-
# second call will be enqueued *behind* the currently running method call.
-
# Any attempt to wait on the result will fail as the second call will never
-
# run until after the current call completes.
-
#
-
# Calling a method using the `await` proxy from within a method that was
-
# itself called using `async` or `await` will irreversibly deadlock the
-
# object. Do *not* do this, ever.
-
#
-
# ## Instance Variables and Attribute Accessors
-
#
-
# Instance variables do not need to be thread-safe so long as they are private.
-
# Asynchronous method calls are processed in the order they are received and
-
# are processed one at a time. Therefore private instance variables can only
-
# be accessed by one thread at a time. This is inherently thread-safe.
-
#
-
# When using private instance variables within asynchronous methods, the best
-
# practice is to read the instance variable into a local variable at the start
-
# of the method then update the instance variable at the *end* of the method.
-
# This way, should an exception be raised during method execution the internal
-
# state of the object will not have been changed.
-
#
-
# ### Reader Attributes
-
#
-
# The use of `attr_reader` is discouraged. Internal state exposed externally,
-
# when necessary, should be done through accessor methods. The instance
-
# variables exposed by these methods *must* be thread-safe, or they must be
-
# called using the `async` and `await` proxy methods. These two approaches are
-
# subtly different.
-
#
-
# When internal state is accessed via the `async` and `await` proxy methods,
-
# the returned value represents the object's state *at the time the call is
-
# processed*, which may *not* be the state of the object at the time the call
-
# is made.
-
#
-
# To get the state *at the current* time, irrespective of an enqueued method
-
# calls, a reader method must be called directly. This is inherently unsafe
-
# unless the instance variable is itself thread-safe, preferably using one
-
# of the thread-safe classes within this library. Because the thread-safe
-
# classes within this library are internally-locking or non-locking, they can
-
# be safely used from within asynchronous methods without causing deadlocks.
-
#
-
# Generally speaking, the best practice is to *not* expose internal state via
-
# reader methods. The best practice is to simply use the method's return value.
-
#
-
# ### Writer Attributes
-
#
-
# Writer attributes should never be used with asynchronous classes. Changing
-
# the state externally, even when done in the thread-safe way, is not logically
-
# consistent. Changes to state need to be timed with respect to all asynchronous
-
# method calls which my be in-process or enqueued. The only safe practice is to
-
# pass all necessary data to each method as arguments and let the method update
-
# the internal state as necessary.
-
#
-
# ## Class Constants, Variables, and Methods
-
#
-
# ### Class Constants
-
#
-
# Class constants do not need to be thread-safe. Since they are read-only and
-
# immutable they may be safely read both externally and from within
-
# asynchronous methods.
-
#
-
# ### Class Variables
-
#
-
# Class variables should be avoided. Class variables represent shared state.
-
# Shared state is anathema to concurrency. Should there be a need to share
-
# state using class variables they *must* be thread-safe, preferably
-
# using the thread-safe classes within this library. When updating class
-
# variables, never assign a new value/object to the variable itself. Assignment
-
# is not thread-safe in Ruby. Instead, use the thread-safe update functions
-
# of the variable itself to change the value.
-
#
-
# The best practice is to *never* use class variables with `Async` classes.
-
#
-
# ### Class Methods
-
#
-
# Class methods which are pure functions are safe. Class methods which modify
-
# class variables should be avoided, for all the reasons listed above.
-
#
-
# ## An Important Note About Thread Safe Guarantees
-
#
-
# > Thread safe guarantees can only be made when asynchronous method calls
-
# > are not mixed with direct method calls. Use only direct method calls
-
# > when the object is used exclusively on a single thread. Use only
-
# > `async` and `await` when the object is shared between threads. Once you
-
# > call a method using `async` or `await`, you should no longer call methods
-
# > directly on the object. Use `async` and `await` exclusively from then on.
-
#
-
# @example
-
#
-
# class Echo
-
# include Concurrent::Async
-
#
-
# def echo(msg)
-
# print "#{msg}\n"
-
# end
-
# end
-
#
-
# horn = Echo.new
-
# horn.echo('zero') # synchronous, not thread-safe
-
# # returns the actual return value of the method
-
#
-
# horn.async.echo('one') # asynchronous, non-blocking, thread-safe
-
# # returns an IVar in the :pending state
-
#
-
# horn.await.echo('two') # synchronous, blocking, thread-safe
-
# # returns an IVar in the :complete state
-
#
-
# @see Concurrent::Actor
-
# @see https://en.wikipedia.org/wiki/Actor_model "Actor Model" at Wikipedia
-
# @see http://www.erlang.org/doc/man/gen_server.html Erlang gen_server
-
# @see http://c2.com/cgi/wiki?LetItCrash "Let It Crash" at http://c2.com/
-
1
module Async
-
-
# @!method self.new(*args, &block)
-
#
-
# Instanciate a new object and ensure proper initialization of the
-
# synchronization mechanisms.
-
#
-
# @param [Array<Object>] args Zero or more arguments to be passed to the
-
# object's initializer.
-
# @param [Proc] block Optional block to pass to the object's initializer.
-
# @return [Object] A properly initialized object of the asynchronous class.
-
-
# Check for the presence of a method on an object and determine if a given
-
# set of arguments matches the required arity.
-
#
-
# @param [Object] obj the object to check against
-
# @param [Symbol] method the method to check the object for
-
# @param [Array] args zero or more arguments for the arity check
-
#
-
# @raise [NameError] the object does not respond to `method` method
-
# @raise [ArgumentError] the given `args` do not match the arity of `method`
-
#
-
# @note This check is imperfect because of the way Ruby reports the arity of
-
# methods with a variable number of arguments. It is possible to determine
-
# if too few arguments are given but impossible to determine if too many
-
# arguments are given. This check may also fail to recognize dynamic behavior
-
# of the object, such as methods simulated with `method_missing`.
-
#
-
# @see http://www.ruby-doc.org/core-2.1.1/Method.html#method-i-arity Method#arity
-
# @see http://ruby-doc.org/core-2.1.0/Object.html#method-i-respond_to-3F Object#respond_to?
-
# @see http://www.ruby-doc.org/core-2.1.0/BasicObject.html#method-i-method_missing BasicObject#method_missing
-
#
-
# @!visibility private
-
1
def self.validate_argc(obj, method, *args)
-
argc = args.length
-
arity = obj.method(method).arity
-
-
if arity >= 0 && argc != arity
-
raise ArgumentError.new("wrong number of arguments (#{argc} for #{arity})")
-
elsif arity < 0 && (arity = (arity + 1).abs) > argc
-
raise ArgumentError.new("wrong number of arguments (#{argc} for #{arity}..*)")
-
end
-
end
-
-
# @!visibility private
-
1
def self.included(base)
-
base.singleton_class.send(:alias_method, :original_new, :new)
-
base.extend(ClassMethods)
-
super(base)
-
end
-
-
# @!visibility private
-
1
module ClassMethods
-
1
def new(*args, &block)
-
obj = original_new(*args, &block)
-
obj.send(:init_synchronization)
-
obj
-
end
-
end
-
1
private_constant :ClassMethods
-
-
# Delegates asynchronous, thread-safe method calls to the wrapped object.
-
#
-
# @!visibility private
-
1
class AsyncDelegator < Synchronization::LockableObject
-
1
safe_initialization!
-
-
# Create a new delegator object wrapping the given delegate.
-
#
-
# @param [Object] delegate the object to wrap and delegate method calls to
-
1
def initialize(delegate)
-
super()
-
@delegate = delegate
-
@queue = []
-
@executor = Concurrent.global_io_executor
-
end
-
-
# Delegates method calls to the wrapped object.
-
#
-
# @param [Symbol] method the method being called
-
# @param [Array] args zero or more arguments to the method
-
#
-
# @return [IVar] the result of the method call
-
#
-
# @raise [NameError] the object does not respond to `method` method
-
# @raise [ArgumentError] the given `args` do not match the arity of `method`
-
1
def method_missing(method, *args, &block)
-
super unless @delegate.respond_to?(method)
-
Async::validate_argc(@delegate, method, *args)
-
-
ivar = Concurrent::IVar.new
-
synchronize do
-
@queue.push [ivar, method, args, block]
-
@executor.post { perform } if @queue.length == 1
-
end
-
-
ivar
-
end
-
-
# Check whether the method is responsive
-
#
-
# @param [Symbol] method the method being called
-
1
def respond_to_missing?(method, include_private = false)
-
@delegate.respond_to?(method) || super
-
end
-
-
# Perform all enqueued tasks.
-
#
-
# This method must be called from within the executor. It must not be
-
# called while already running. It will loop until the queue is empty.
-
1
def perform
-
loop do
-
ivar, method, args, block = synchronize { @queue.first }
-
break unless ivar # queue is empty
-
-
begin
-
ivar.set(@delegate.send(method, *args, &block))
-
rescue => error
-
ivar.fail(error)
-
end
-
-
synchronize do
-
@queue.shift
-
return if @queue.empty?
-
end
-
end
-
end
-
end
-
1
private_constant :AsyncDelegator
-
-
# Delegates synchronous, thread-safe method calls to the wrapped object.
-
#
-
# @!visibility private
-
1
class AwaitDelegator
-
-
# Create a new delegator object wrapping the given delegate.
-
#
-
# @param [AsyncDelegator] delegate the object to wrap and delegate method calls to
-
1
def initialize(delegate)
-
@delegate = delegate
-
end
-
-
# Delegates method calls to the wrapped object.
-
#
-
# @param [Symbol] method the method being called
-
# @param [Array] args zero or more arguments to the method
-
#
-
# @return [IVar] the result of the method call
-
#
-
# @raise [NameError] the object does not respond to `method` method
-
# @raise [ArgumentError] the given `args` do not match the arity of `method`
-
1
def method_missing(method, *args, &block)
-
ivar = @delegate.send(method, *args, &block)
-
ivar.wait
-
ivar
-
end
-
-
# Check whether the method is responsive
-
#
-
# @param [Symbol] method the method being called
-
1
def respond_to_missing?(method, include_private = false)
-
@delegate.respond_to?(method) || super
-
end
-
end
-
1
private_constant :AwaitDelegator
-
-
# Causes the chained method call to be performed asynchronously on the
-
# object's thread. The delegated method will return a future in the
-
# `:pending` state and the method call will have been scheduled on the
-
# object's thread. The final disposition of the method call can be obtained
-
# by inspecting the returned future.
-
#
-
# @!macro async_thread_safety_warning
-
# @note The method call is guaranteed to be thread safe with respect to
-
# all other method calls against the same object that are called with
-
# either `async` or `await`. The mutable nature of Ruby references
-
# (and object orientation in general) prevent any other thread safety
-
# guarantees. Do NOT mix direct method calls with delegated method calls.
-
# Use *only* delegated method calls when sharing the object between threads.
-
#
-
# @return [Concurrent::IVar] the pending result of the asynchronous operation
-
#
-
# @raise [NameError] the object does not respond to the requested method
-
# @raise [ArgumentError] the given `args` do not match the arity of
-
# the requested method
-
1
def async
-
@__async_delegator__
-
end
-
1
alias_method :cast, :async
-
-
# Causes the chained method call to be performed synchronously on the
-
# current thread. The delegated will return a future in either the
-
# `:fulfilled` or `:rejected` state and the delegated method will have
-
# completed. The final disposition of the delegated method can be obtained
-
# by inspecting the returned future.
-
#
-
# @!macro async_thread_safety_warning
-
#
-
# @return [Concurrent::IVar] the completed result of the synchronous operation
-
#
-
# @raise [NameError] the object does not respond to the requested method
-
# @raise [ArgumentError] the given `args` do not match the arity of the
-
# requested method
-
1
def await
-
@__await_delegator__
-
end
-
1
alias_method :call, :await
-
-
# Initialize the internal serializer and other stnchronization mechanisms.
-
#
-
# @note This method *must* be called immediately upon object construction.
-
# This is the only way thread-safe initialization can be guaranteed.
-
#
-
# @!visibility private
-
1
def init_synchronization
-
return self if defined?(@__async_initialized__) && @__async_initialized__
-
@__async_initialized__ = true
-
@__async_delegator__ = AsyncDelegator.new(self)
-
@__await_delegator__ = AwaitDelegator.new(@__async_delegator__)
-
self
-
end
-
end
-
end
-
1
require 'concurrent/atomic/atomic_reference'
-
1
require 'concurrent/collection/copy_on_notify_observer_set'
-
1
require 'concurrent/concern/observable'
-
1
require 'concurrent/synchronization'
-
-
# @!macro thread_safe_variable_comparison
-
#
-
# ## Thread-safe Variable Classes
-
#
-
# Each of the thread-safe variable classes is designed to solve a different
-
# problem. In general:
-
#
-
# * *{Concurrent::Agent}:* Shared, mutable variable providing independent,
-
# uncoordinated, *asynchronous* change of individual values. Best used when
-
# the value will undergo frequent, complex updates. Suitable when the result
-
# of an update does not need to be known immediately.
-
# * *{Concurrent::Atom}:* Shared, mutable variable providing independent,
-
# uncoordinated, *synchronous* change of individual values. Best used when
-
# the value will undergo frequent reads but only occasional, though complex,
-
# updates. Suitable when the result of an update must be known immediately.
-
# * *{Concurrent::AtomicReference}:* A simple object reference that can be updated
-
# atomically. Updates are synchronous but fast. Best used when updates a
-
# simple set operations. Not suitable when updates are complex.
-
# {Concurrent::AtomicBoolean} and {Concurrent::AtomicFixnum} are similar
-
# but optimized for the given data type.
-
# * *{Concurrent::Exchanger}:* Shared, stateless synchronization point. Used
-
# when two or more threads need to exchange data. The threads will pair then
-
# block on each other until the exchange is complete.
-
# * *{Concurrent::MVar}:* Shared synchronization point. Used when one thread
-
# must give a value to another, which must take the value. The threads will
-
# block on each other until the exchange is complete.
-
# * *{Concurrent::ThreadLocalVar}:* Shared, mutable, isolated variable which
-
# holds a different value for each thread which has access. Often used as
-
# an instance variable in objects which must maintain different state
-
# for different threads.
-
# * *{Concurrent::TVar}:* Shared, mutable variables which provide
-
# *coordinated*, *synchronous*, change of *many* stated. Used when multiple
-
# value must change together, in an all-or-nothing transaction.
-
-
-
1
module Concurrent
-
-
# Atoms provide a way to manage shared, synchronous, independent state.
-
#
-
# An atom is initialized with an initial value and an optional validation
-
# proc. At any time the value of the atom can be synchronously and safely
-
# changed. If a validator is given at construction then any new value
-
# will be checked against the validator and will be rejected if the
-
# validator returns false or raises an exception.
-
#
-
# There are two ways to change the value of an atom: {#compare_and_set} and
-
# {#swap}. The former will set the new value if and only if it validates and
-
# the current value matches the new value. The latter will atomically set the
-
# new value to the result of running the given block if and only if that
-
# value validates.
-
#
-
# ## Example
-
#
-
# ```
-
# def next_fibonacci(set = nil)
-
# return [0, 1] if set.nil?
-
# set + [set[-2..-1].reduce{|sum,x| sum + x }]
-
# end
-
#
-
# # create an atom with an initial value
-
# atom = Concurrent::Atom.new(next_fibonacci)
-
#
-
# # send a few update requests
-
# 5.times do
-
# atom.swap{|set| next_fibonacci(set) }
-
# end
-
#
-
# # get the current value
-
# atom.value #=> [0, 1, 1, 2, 3, 5, 8]
-
# ```
-
#
-
# ## Observation
-
#
-
# Atoms support observers through the {Concurrent::Observable} mixin module.
-
# Notification of observers occurs every time the value of the Atom changes.
-
# When notified the observer will receive three arguments: `time`, `old_value`,
-
# and `new_value`. The `time` argument is the time at which the value change
-
# occurred. The `old_value` is the value of the Atom when the change began
-
# The `new_value` is the value to which the Atom was set when the change
-
# completed. Note that `old_value` and `new_value` may be the same. This is
-
# not an error. It simply means that the change operation returned the same
-
# value.
-
#
-
# Unlike in Clojure, `Atom` cannot participate in {Concurrent::TVar} transactions.
-
#
-
# @!macro thread_safe_variable_comparison
-
#
-
# @see http://clojure.org/atoms Clojure Atoms
-
# @see http://clojure.org/state Values and Change - Clojure's approach to Identity and State
-
1
class Atom < Synchronization::Object
-
1
include Concern::Observable
-
-
1
safe_initialization!
-
1
attr_atomic(:value)
-
1
private :value=, :swap_value, :compare_and_set_value, :update_value
-
1
public :value
-
1
alias_method :deref, :value
-
-
# @!method value
-
# The current value of the atom.
-
#
-
# @return [Object] The current value.
-
-
# Create a new atom with the given initial value.
-
#
-
# @param [Object] value The initial value
-
# @param [Hash] opts The options used to configure the atom
-
# @option opts [Proc] :validator (nil) Optional proc used to validate new
-
# values. It must accept one and only one argument which will be the
-
# intended new value. The validator will return true if the new value
-
# is acceptable else return false (preferrably) or raise an exception.
-
#
-
# @!macro deref_options
-
#
-
# @raise [ArgumentError] if the validator is not a `Proc` (when given)
-
1
def initialize(value, opts = {})
-
super()
-
@Validator = opts.fetch(:validator, -> v { true })
-
self.observers = Collection::CopyOnNotifyObserverSet.new
-
self.value = value
-
end
-
-
# Atomically swaps the value of atom using the given block. The current
-
# value will be passed to the block, as will any arguments passed as
-
# arguments to the function. The new value will be validated against the
-
# (optional) validator proc given at construction. If validation fails the
-
# value will not be changed.
-
#
-
# Internally, {#swap} reads the current value, applies the block to it, and
-
# attempts to compare-and-set it in. Since another thread may have changed
-
# the value in the intervening time, it may have to retry, and does so in a
-
# spin loop. The net effect is that the value will always be the result of
-
# the application of the supplied block to a current value, atomically.
-
# However, because the block might be called multiple times, it must be free
-
# of side effects.
-
#
-
# @note The given block may be called multiple times, and thus should be free
-
# of side effects.
-
#
-
# @param [Object] args Zero or more arguments passed to the block.
-
#
-
# @yield [value, args] Calculates a new value for the atom based on the
-
# current value and any supplied arguments.
-
# @yieldparam value [Object] The current value of the atom.
-
# @yieldparam args [Object] All arguments passed to the function, in order.
-
# @yieldreturn [Object] The intended new value of the atom.
-
#
-
# @return [Object] The final value of the atom after all operations and
-
# validations are complete.
-
#
-
# @raise [ArgumentError] When no block is given.
-
1
def swap(*args)
-
raise ArgumentError.new('no block given') unless block_given?
-
-
loop do
-
old_value = value
-
new_value = yield(old_value, *args)
-
begin
-
break old_value unless valid?(new_value)
-
break new_value if compare_and_set(old_value, new_value)
-
rescue
-
break old_value
-
end
-
end
-
end
-
-
# Atomically sets the value of atom to the new value if and only if the
-
# current value of the atom is identical to the old value and the new
-
# value successfully validates against the (optional) validator given
-
# at construction.
-
#
-
# @param [Object] old_value The expected current value.
-
# @param [Object] new_value The intended new value.
-
#
-
# @return [Boolean] True if the value is changed else false.
-
1
def compare_and_set(old_value, new_value)
-
if valid?(new_value) && compare_and_set_value(old_value, new_value)
-
observers.notify_observers(Time.now, old_value, new_value)
-
true
-
else
-
false
-
end
-
end
-
-
# Atomically sets the value of atom to the new value without regard for the
-
# current value so long as the new value successfully validates against the
-
# (optional) validator given at construction.
-
#
-
# @param [Object] new_value The intended new value.
-
#
-
# @return [Object] The final value of the atom after all operations and
-
# validations are complete.
-
1
def reset(new_value)
-
old_value = value
-
if valid?(new_value)
-
self.value = new_value
-
observers.notify_observers(Time.now, old_value, new_value)
-
new_value
-
else
-
old_value
-
end
-
end
-
-
1
private
-
-
# Is the new value valid?
-
#
-
# @param [Object] new_value The intended new value.
-
# @return [Boolean] false if the validator function returns false or raises
-
# an exception else true
-
1
def valid?(new_value)
-
@Validator.call(new_value)
-
rescue
-
false
-
end
-
end
-
end
-
1
require 'concurrent/constants'
-
-
1
module Concurrent
-
-
# @!macro thread_local_var
-
# @!macro internal_implementation_note
-
# @!visibility private
-
1
class AbstractThreadLocalVar
-
-
# @!macro thread_local_var_method_initialize
-
1
def initialize(default = nil, &default_block)
-
if default && block_given?
-
raise ArgumentError, "Cannot use both value and block as default value"
-
end
-
-
if block_given?
-
@default_block = default_block
-
@default = nil
-
else
-
@default_block = nil
-
@default = default
-
end
-
-
allocate_storage
-
end
-
-
# @!macro thread_local_var_method_get
-
1
def value
-
raise NotImplementedError
-
end
-
-
# @!macro thread_local_var_method_set
-
1
def value=(value)
-
raise NotImplementedError
-
end
-
-
# @!macro thread_local_var_method_bind
-
1
def bind(value, &block)
-
if block_given?
-
old_value = self.value
-
begin
-
self.value = value
-
yield
-
ensure
-
self.value = old_value
-
end
-
end
-
end
-
-
1
protected
-
-
# @!visibility private
-
1
def allocate_storage
-
raise NotImplementedError
-
end
-
-
# @!visibility private
-
1
def default
-
if @default_block
-
self.value = @default_block.call
-
else
-
@default
-
end
-
end
-
end
-
end
-
1
module Concurrent
-
# An atomic reference which maintains an object reference along with a mark bit
-
# that can be updated atomically.
-
#
-
# @see http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/atomic/AtomicMarkableReference.html
-
# java.util.concurrent.atomic.AtomicMarkableReference
-
1
class AtomicMarkableReference < ::Concurrent::Synchronization::Object
-
-
1
attr_atomic(:reference)
-
1
private :reference, :reference=, :swap_reference, :compare_and_set_reference, :update_reference
-
-
1
def initialize(value = nil, mark = false)
-
super()
-
self.reference = immutable_array(value, mark)
-
end
-
-
# Atomically sets the value and mark to the given updated value and
-
# mark given both:
-
# - the current value == the expected value &&
-
# - the current mark == the expected mark
-
#
-
# @param [Object] expected_val the expected value
-
# @param [Object] new_val the new value
-
# @param [Boolean] expected_mark the expected mark
-
# @param [Boolean] new_mark the new mark
-
#
-
# @return [Boolean] `true` if successful. A `false` return indicates
-
# that the actual value was not equal to the expected value or the
-
# actual mark was not equal to the expected mark
-
1
def compare_and_set(expected_val, new_val, expected_mark, new_mark)
-
# Memoize a valid reference to the current AtomicReference for
-
# later comparison.
-
current = reference
-
curr_val, curr_mark = current
-
-
# Ensure that that the expected marks match.
-
return false unless expected_mark == curr_mark
-
-
if expected_val.is_a? Numeric
-
# If the object is a numeric, we need to ensure we are comparing
-
# the numerical values
-
return false unless expected_val == curr_val
-
else
-
# Otherwise, we need to ensure we are comparing the object identity.
-
# Theoretically, this could be incorrect if a user monkey-patched
-
# `Object#equal?`, but they should know that they are playing with
-
# fire at that point.
-
return false unless expected_val.equal? curr_val
-
end
-
-
prospect = immutable_array(new_val, new_mark)
-
-
compare_and_set_reference current, prospect
-
end
-
-
1
alias_method :compare_and_swap, :compare_and_set
-
-
# Gets the current reference and marked values.
-
#
-
# @return [Array] the current reference and marked values
-
1
def get
-
reference
-
end
-
-
# Gets the current value of the reference
-
#
-
# @return [Object] the current value of the reference
-
1
def value
-
reference[0]
-
end
-
-
# Gets the current marked value
-
#
-
# @return [Boolean] the current marked value
-
1
def mark
-
reference[1]
-
end
-
-
1
alias_method :marked?, :mark
-
-
# _Unconditionally_ sets to the given value of both the reference and
-
# the mark.
-
#
-
# @param [Object] new_val the new value
-
# @param [Boolean] new_mark the new mark
-
#
-
# @return [Array] both the new value and the new mark
-
1
def set(new_val, new_mark)
-
self.reference = immutable_array(new_val, new_mark)
-
end
-
-
# Pass the current value and marked state to the given block, replacing it
-
# with the block's results. May retry if the value changes during the
-
# block's execution.
-
#
-
# @yield [Object] Calculate a new value and marked state for the atomic
-
# reference using given (old) value and (old) marked
-
# @yieldparam [Object] old_val the starting value of the atomic reference
-
# @yieldparam [Boolean] old_mark the starting state of marked
-
#
-
# @return [Array] the new value and new mark
-
1
def update
-
loop do
-
old_val, old_mark = reference
-
new_val, new_mark = yield old_val, old_mark
-
-
if compare_and_set old_val, new_val, old_mark, new_mark
-
return immutable_array(new_val, new_mark)
-
end
-
end
-
end
-
-
# Pass the current value to the given block, replacing it
-
# with the block's result. Raise an exception if the update
-
# fails.
-
#
-
# @yield [Object] Calculate a new value and marked state for the atomic
-
# reference using given (old) value and (old) marked
-
# @yieldparam [Object] old_val the starting value of the atomic reference
-
# @yieldparam [Boolean] old_mark the starting state of marked
-
#
-
# @return [Array] the new value and marked state
-
#
-
# @raise [Concurrent::ConcurrentUpdateError] if the update fails
-
1
def try_update!
-
old_val, old_mark = reference
-
new_val, new_mark = yield old_val, old_mark
-
-
unless compare_and_set old_val, new_val, old_mark, new_mark
-
fail ::Concurrent::ConcurrentUpdateError,
-
'AtomicMarkableReference: Update failed due to race condition.',
-
'Note: If you would like to guarantee an update, please use ' +
-
'the `AtomicMarkableReference#update` method.'
-
end
-
-
immutable_array(new_val, new_mark)
-
end
-
-
# Pass the current value to the given block, replacing it with the
-
# block's result. Simply return nil if update fails.
-
#
-
# @yield [Object] Calculate a new value and marked state for the atomic
-
# reference using given (old) value and (old) marked
-
# @yieldparam [Object] old_val the starting value of the atomic reference
-
# @yieldparam [Boolean] old_mark the starting state of marked
-
#
-
# @return [Array] the new value and marked state, or nil if
-
# the update failed
-
1
def try_update
-
old_val, old_mark = reference
-
new_val, new_mark = yield old_val, old_mark
-
-
return unless compare_and_set old_val, new_val, old_mark, new_mark
-
-
immutable_array(new_val, new_mark)
-
end
-
-
1
private
-
-
1
def immutable_array(*args)
-
args.freeze
-
end
-
end
-
end
-
1
require 'concurrent/synchronization'
-
1
require 'concurrent/utility/engine'
-
1
require 'concurrent/atomic_reference/numeric_cas_wrapper'
-
-
# Shim for TruffleRuby::AtomicReference
-
1
if Concurrent.on_truffleruby? && !defined?(TruffleRuby::AtomicReference)
-
# @!visibility private
-
module TruffleRuby
-
AtomicReference = Truffle::AtomicReference
-
end
-
end
-
-
1
module Concurrent
-
-
# Define update methods that use direct paths
-
#
-
# @!visibility private
-
# @!macro internal_implementation_note
-
1
module AtomicDirectUpdate
-
-
# @!macro atomic_reference_method_update
-
#
-
# Pass the current value to the given block, replacing it
-
# with the block's result. May retry if the value changes
-
# during the block's execution.
-
#
-
# @yield [Object] Calculate a new value for the atomic reference using
-
# given (old) value
-
# @yieldparam [Object] old_value the starting value of the atomic reference
-
# @return [Object] the new value
-
1
def update
-
true until compare_and_set(old_value = get, new_value = yield(old_value))
-
new_value
-
end
-
-
# @!macro atomic_reference_method_try_update
-
#
-
# Pass the current value to the given block, replacing it
-
# with the block's result. Return nil if the update fails.
-
#
-
# @yield [Object] Calculate a new value for the atomic reference using
-
# given (old) value
-
# @yieldparam [Object] old_value the starting value of the atomic reference
-
# @note This method was altered to avoid raising an exception by default.
-
# Instead, this method now returns `nil` in case of failure. For more info,
-
# please see: https://github.com/ruby-concurrency/concurrent-ruby/pull/336
-
# @return [Object] the new value, or nil if update failed
-
1
def try_update
-
old_value = get
-
new_value = yield old_value
-
-
return unless compare_and_set old_value, new_value
-
-
new_value
-
end
-
-
# @!macro atomic_reference_method_try_update!
-
#
-
# Pass the current value to the given block, replacing it
-
# with the block's result. Raise an exception if the update
-
# fails.
-
#
-
# @yield [Object] Calculate a new value for the atomic reference using
-
# given (old) value
-
# @yieldparam [Object] old_value the starting value of the atomic reference
-
# @note This behavior mimics the behavior of the original
-
# `AtomicReference#try_update` API. The reason this was changed was to
-
# avoid raising exceptions (which are inherently slow) by default. For more
-
# info: https://github.com/ruby-concurrency/concurrent-ruby/pull/336
-
# @return [Object] the new value
-
# @raise [Concurrent::ConcurrentUpdateError] if the update fails
-
1
def try_update!
-
old_value = get
-
new_value = yield old_value
-
unless compare_and_set(old_value, new_value)
-
if $VERBOSE
-
raise ConcurrentUpdateError, "Update failed"
-
else
-
raise ConcurrentUpdateError, "Update failed", ConcurrentUpdateError::CONC_UP_ERR_BACKTRACE
-
end
-
end
-
new_value
-
end
-
end
-
-
1
require 'concurrent/atomic_reference/mutex_atomic'
-
-
# @!macro atomic_reference
-
#
-
# An object reference that may be updated atomically. All read and write
-
# operations have java volatile semantic.
-
#
-
# @!macro thread_safe_variable_comparison
-
#
-
# @see http://docs.oracle.com/javase/8/docs/api/java/util/concurrent/atomic/AtomicReference.html
-
# @see http://docs.oracle.com/javase/8/docs/api/java/util/concurrent/atomic/package-summary.html
-
#
-
# @!method initialize(value = nil)
-
# @!macro atomic_reference_method_initialize
-
# @param [Object] value The initial value.
-
#
-
# @!method get
-
# @!macro atomic_reference_method_get
-
# Gets the current value.
-
# @return [Object] the current value
-
#
-
# @!method set(new_value)
-
# @!macro atomic_reference_method_set
-
# Sets to the given value.
-
# @param [Object] new_value the new value
-
# @return [Object] the new value
-
#
-
# @!method get_and_set(new_value)
-
# @!macro atomic_reference_method_get_and_set
-
# Atomically sets to the given value and returns the old value.
-
# @param [Object] new_value the new value
-
# @return [Object] the old value
-
#
-
# @!method compare_and_set(old_value, new_value)
-
# @!macro atomic_reference_method_compare_and_set
-
#
-
# Atomically sets the value to the given updated value if
-
# the current value == the expected value.
-
#
-
# @param [Object] old_value the expected value
-
# @param [Object] new_value the new value
-
#
-
# @return [Boolean] `true` if successful. A `false` return indicates
-
# that the actual value was not equal to the expected value.
-
#
-
# @!method update
-
# @!macro atomic_reference_method_update
-
#
-
# @!method try_update
-
# @!macro atomic_reference_method_try_update
-
#
-
# @!method try_update!
-
# @!macro atomic_reference_method_try_update!
-
-
-
# @!macro internal_implementation_note
-
1
class ConcurrentUpdateError < ThreadError
-
# frozen pre-allocated backtrace to speed ConcurrentUpdateError
-
1
CONC_UP_ERR_BACKTRACE = ['backtrace elided; set verbose to enable'].freeze
-
end
-
-
# @!macro internal_implementation_note
-
AtomicReferenceImplementation = case
-
1
when Concurrent.on_cruby? && Concurrent.c_extensions_loaded?
-
# @!visibility private
-
# @!macro internal_implementation_note
-
class CAtomicReference
-
include AtomicDirectUpdate
-
include AtomicNumericCompareAndSetWrapper
-
alias_method :compare_and_swap, :compare_and_set
-
end
-
CAtomicReference
-
when Concurrent.on_jruby?
-
# @!visibility private
-
# @!macro internal_implementation_note
-
class JavaAtomicReference
-
include AtomicDirectUpdate
-
end
-
JavaAtomicReference
-
when Concurrent.on_truffleruby?
-
class TruffleRubyAtomicReference < TruffleRuby::AtomicReference
-
include AtomicDirectUpdate
-
alias_method :value, :get
-
alias_method :value=, :set
-
alias_method :compare_and_swap, :compare_and_set
-
alias_method :swap, :get_and_set
-
end
-
when Concurrent.on_rbx?
-
# @note Extends `Rubinius::AtomicReference` version adding aliases
-
# and numeric logic.
-
#
-
# @!visibility private
-
# @!macro internal_implementation_note
-
class RbxAtomicReference < Rubinius::AtomicReference
-
alias_method :_compare_and_set, :compare_and_set
-
include AtomicDirectUpdate
-
include AtomicNumericCompareAndSetWrapper
-
alias_method :value, :get
-
alias_method :value=, :set
-
alias_method :swap, :get_and_set
-
alias_method :compare_and_swap, :compare_and_set
-
end
-
RbxAtomicReference
-
else
-
1
MutexAtomicReference
-
end
-
1
private_constant :AtomicReferenceImplementation
-
-
# @!macro atomic_reference
-
1
class AtomicReference < AtomicReferenceImplementation
-
-
# @return [String] Short string representation.
-
1
def to_s
-
format '%s value:%s>', super[0..-2], get
-
end
-
-
1
alias_method :inspect, :to_s
-
end
-
end
-
1
require 'concurrent/utility/engine'
-
1
require 'concurrent/atomic/mutex_count_down_latch'
-
1
require 'concurrent/atomic/java_count_down_latch'
-
-
1
module Concurrent
-
-
###################################################################
-
-
# @!macro count_down_latch_method_initialize
-
#
-
# Create a new `CountDownLatch` with the initial `count`.
-
#
-
# @param [new] count the initial count
-
#
-
# @raise [ArgumentError] if `count` is not an integer or is less than zero
-
-
# @!macro count_down_latch_method_wait
-
#
-
# Block on the latch until the counter reaches zero or until `timeout` is reached.
-
#
-
# @param [Fixnum] timeout the number of seconds to wait for the counter or `nil`
-
# to block indefinitely
-
# @return [Boolean] `true` if the `count` reaches zero else false on `timeout`
-
-
# @!macro count_down_latch_method_count_down
-
#
-
# Signal the latch to decrement the counter. Will signal all blocked threads when
-
# the `count` reaches zero.
-
-
# @!macro count_down_latch_method_count
-
#
-
# The current value of the counter.
-
#
-
# @return [Fixnum] the current value of the counter
-
-
###################################################################
-
-
# @!macro count_down_latch_public_api
-
#
-
# @!method initialize(count = 1)
-
# @!macro count_down_latch_method_initialize
-
#
-
# @!method wait(timeout = nil)
-
# @!macro count_down_latch_method_wait
-
#
-
# @!method count_down
-
# @!macro count_down_latch_method_count_down
-
#
-
# @!method count
-
# @!macro count_down_latch_method_count
-
-
###################################################################
-
-
# @!visibility private
-
# @!macro internal_implementation_note
-
CountDownLatchImplementation = case
-
1
when Concurrent.on_jruby?
-
JavaCountDownLatch
-
else
-
1
MutexCountDownLatch
-
end
-
1
private_constant :CountDownLatchImplementation
-
-
# @!macro count_down_latch
-
#
-
# A synchronization object that allows one thread to wait on multiple other threads.
-
# The thread that will wait creates a `CountDownLatch` and sets the initial value
-
# (normally equal to the number of other threads). The initiating thread passes the
-
# latch to the other threads then waits for the other threads by calling the `#wait`
-
# method. Each of the other threads calls `#count_down` when done with its work.
-
# When the latch counter reaches zero the waiting thread is unblocked and continues
-
# with its work. A `CountDownLatch` can be used only once. Its value cannot be reset.
-
#
-
# @!macro count_down_latch_public_api
-
# @example Waiter and Decrementer
-
# latch = Concurrent::CountDownLatch.new(3)
-
#
-
# waiter = Thread.new do
-
# latch.wait()
-
# puts ("Waiter released")
-
# end
-
#
-
# decrementer = Thread.new do
-
# sleep(1)
-
# latch.count_down
-
# puts latch.count
-
#
-
# sleep(1)
-
# latch.count_down
-
# puts latch.count
-
#
-
# sleep(1)
-
# latch.count_down
-
# puts latch.count
-
# end
-
#
-
# [waiter, decrementer].each(&:join)
-
1
class CountDownLatch < CountDownLatchImplementation
-
end
-
end
-
1
require 'concurrent/synchronization'
-
1
require 'concurrent/utility/native_integer'
-
-
1
module Concurrent
-
-
# A synchronization aid that allows a set of threads to all wait for each
-
# other to reach a common barrier point.
-
# @example
-
# barrier = Concurrent::CyclicBarrier.new(3)
-
# jobs = Array.new(3) { |i| -> { sleep i; p done: i } }
-
# process = -> (i) do
-
# # waiting to start at the same time
-
# barrier.wait
-
# # execute job
-
# jobs[i].call
-
# # wait for others to finish
-
# barrier.wait
-
# end
-
# threads = 2.times.map do |i|
-
# Thread.new(i, &process)
-
# end
-
#
-
# # use main as well
-
# process.call 2
-
#
-
# # here we can be sure that all jobs are processed
-
1
class CyclicBarrier < Synchronization::LockableObject
-
-
# @!visibility private
-
1
Generation = Struct.new(:status)
-
1
private_constant :Generation
-
-
# Create a new `CyclicBarrier` that waits for `parties` threads
-
#
-
# @param [Fixnum] parties the number of parties
-
# @yield an optional block that will be executed that will be executed after
-
# the last thread arrives and before the others are released
-
#
-
# @raise [ArgumentError] if `parties` is not an integer or is less than zero
-
1
def initialize(parties, &block)
-
Utility::NativeInteger.ensure_integer_and_bounds parties
-
Utility::NativeInteger.ensure_positive_and_no_zero parties
-
-
super(&nil)
-
synchronize { ns_initialize parties, &block }
-
end
-
-
# @return [Fixnum] the number of threads needed to pass the barrier
-
1
def parties
-
synchronize { @parties }
-
end
-
-
# @return [Fixnum] the number of threads currently waiting on the barrier
-
1
def number_waiting
-
synchronize { @number_waiting }
-
end
-
-
# Blocks on the barrier until the number of waiting threads is equal to
-
# `parties` or until `timeout` is reached or `reset` is called
-
# If a block has been passed to the constructor, it will be executed once by
-
# the last arrived thread before releasing the others
-
# @param [Fixnum] timeout the number of seconds to wait for the counter or
-
# `nil` to block indefinitely
-
# @return [Boolean] `true` if the `count` reaches zero else false on
-
# `timeout` or on `reset` or if the barrier is broken
-
1
def wait(timeout = nil)
-
synchronize do
-
-
return false unless @generation.status == :waiting
-
-
@number_waiting += 1
-
-
if @number_waiting == @parties
-
@action.call if @action
-
ns_generation_done @generation, :fulfilled
-
true
-
else
-
generation = @generation
-
if ns_wait_until(timeout) { generation.status != :waiting }
-
generation.status == :fulfilled
-
else
-
ns_generation_done generation, :broken, false
-
false
-
end
-
end
-
end
-
end
-
-
# resets the barrier to its initial state
-
# If there is at least one waiting thread, it will be woken up, the `wait`
-
# method will return false and the barrier will be broken
-
# If the barrier is broken, this method restores it to the original state
-
#
-
# @return [nil]
-
1
def reset
-
synchronize { ns_generation_done @generation, :reset }
-
end
-
-
# A barrier can be broken when:
-
# - a thread called the `reset` method while at least one other thread was waiting
-
# - at least one thread timed out on `wait` method
-
#
-
# A broken barrier can be restored using `reset` it's safer to create a new one
-
# @return [Boolean] true if the barrier is broken otherwise false
-
1
def broken?
-
synchronize { @generation.status != :waiting }
-
end
-
-
1
protected
-
-
1
def ns_generation_done(generation, status, continue = true)
-
generation.status = status
-
ns_next_generation if continue
-
ns_broadcast
-
end
-
-
1
def ns_next_generation
-
@generation = Generation.new(:waiting)
-
@number_waiting = 0
-
end
-
-
1
def ns_initialize(parties, &block)
-
@parties = parties
-
@action = block
-
ns_next_generation
-
end
-
end
-
end
-
1
if Concurrent.on_jruby?
-
-
module Concurrent
-
-
# @!macro count_down_latch
-
# @!visibility private
-
# @!macro internal_implementation_note
-
class JavaCountDownLatch
-
-
# @!macro count_down_latch_method_initialize
-
def initialize(count = 1)
-
Utility::NativeInteger.ensure_integer_and_bounds(count)
-
Utility::NativeInteger.ensure_positive(count)
-
@latch = java.util.concurrent.CountDownLatch.new(count)
-
end
-
-
# @!macro count_down_latch_method_wait
-
def wait(timeout = nil)
-
result = nil
-
if timeout.nil?
-
Synchronization::JRuby.sleep_interruptibly { @latch.await }
-
result = true
-
else
-
Synchronization::JRuby.sleep_interruptibly do
-
result = @latch.await(1000 * timeout, java.util.concurrent.TimeUnit::MILLISECONDS)
-
end
-
end
-
result
-
end
-
-
# @!macro count_down_latch_method_count_down
-
def count_down
-
@latch.countDown
-
end
-
-
# @!macro count_down_latch_method_count
-
def count
-
@latch.getCount
-
end
-
end
-
end
-
end
-
1
require 'concurrent/atomic/abstract_thread_local_var'
-
-
1
if Concurrent.on_jruby?
-
-
module Concurrent
-
-
# @!visibility private
-
# @!macro internal_implementation_note
-
class JavaThreadLocalVar < AbstractThreadLocalVar
-
-
# @!macro thread_local_var_method_get
-
def value
-
value = @var.get
-
-
if value.nil?
-
default
-
elsif value == NULL
-
nil
-
else
-
value
-
end
-
end
-
-
# @!macro thread_local_var_method_set
-
def value=(value)
-
@var.set(value)
-
end
-
-
protected
-
-
# @!visibility private
-
def allocate_storage
-
@var = java.lang.ThreadLocal.new
-
end
-
end
-
end
-
end
-
1
require 'concurrent/synchronization'
-
-
1
module Concurrent
-
-
# @!macro atomic_boolean
-
# @!visibility private
-
# @!macro internal_implementation_note
-
1
class MutexAtomicBoolean < Synchronization::LockableObject
-
-
# @!macro atomic_boolean_method_initialize
-
1
def initialize(initial = false)
-
super()
-
synchronize { ns_initialize(initial) }
-
end
-
-
# @!macro atomic_boolean_method_value_get
-
1
def value
-
synchronize { @value }
-
end
-
-
# @!macro atomic_boolean_method_value_set
-
1
def value=(value)
-
synchronize { @value = !!value }
-
end
-
-
# @!macro atomic_boolean_method_true_question
-
1
def true?
-
synchronize { @value }
-
end
-
-
# @!macro atomic_boolean_method_false_question
-
1
def false?
-
synchronize { !@value }
-
end
-
-
# @!macro atomic_boolean_method_make_true
-
1
def make_true
-
synchronize { ns_make_value(true) }
-
end
-
-
# @!macro atomic_boolean_method_make_false
-
1
def make_false
-
synchronize { ns_make_value(false) }
-
end
-
-
1
protected
-
-
# @!visibility private
-
1
def ns_initialize(initial)
-
@value = !!initial
-
end
-
-
1
private
-
-
# @!visibility private
-
1
def ns_make_value(value)
-
old = @value
-
@value = value
-
old != @value
-
end
-
end
-
end
-
1
require 'concurrent/synchronization'
-
1
require 'concurrent/utility/native_integer'
-
-
1
module Concurrent
-
-
# @!macro atomic_fixnum
-
# @!visibility private
-
# @!macro internal_implementation_note
-
1
class MutexAtomicFixnum < Synchronization::LockableObject
-
-
# @!macro atomic_fixnum_method_initialize
-
1
def initialize(initial = 0)
-
super()
-
synchronize { ns_initialize(initial) }
-
end
-
-
# @!macro atomic_fixnum_method_value_get
-
1
def value
-
synchronize { @value }
-
end
-
-
# @!macro atomic_fixnum_method_value_set
-
1
def value=(value)
-
synchronize { ns_set(value) }
-
end
-
-
# @!macro atomic_fixnum_method_increment
-
1
def increment(delta = 1)
-
synchronize { ns_set(@value + delta.to_i) }
-
end
-
-
1
alias_method :up, :increment
-
-
# @!macro atomic_fixnum_method_decrement
-
1
def decrement(delta = 1)
-
synchronize { ns_set(@value - delta.to_i) }
-
end
-
-
1
alias_method :down, :decrement
-
-
# @!macro atomic_fixnum_method_compare_and_set
-
1
def compare_and_set(expect, update)
-
synchronize do
-
if @value == expect.to_i
-
@value = update.to_i
-
true
-
else
-
false
-
end
-
end
-
end
-
-
# @!macro atomic_fixnum_method_update
-
1
def update
-
synchronize do
-
@value = yield @value
-
end
-
end
-
-
1
protected
-
-
# @!visibility private
-
1
def ns_initialize(initial)
-
ns_set(initial)
-
end
-
-
1
private
-
-
# @!visibility private
-
1
def ns_set(value)
-
Utility::NativeInteger.ensure_integer_and_bounds value
-
@value = value
-
end
-
end
-
end
-
1
require 'concurrent/synchronization'
-
1
require 'concurrent/utility/native_integer'
-
-
1
module Concurrent
-
-
# @!macro count_down_latch
-
# @!visibility private
-
# @!macro internal_implementation_note
-
1
class MutexCountDownLatch < Synchronization::LockableObject
-
-
# @!macro count_down_latch_method_initialize
-
1
def initialize(count = 1)
-
Utility::NativeInteger.ensure_integer_and_bounds count
-
Utility::NativeInteger.ensure_positive count
-
-
super()
-
synchronize { ns_initialize count }
-
end
-
-
# @!macro count_down_latch_method_wait
-
1
def wait(timeout = nil)
-
synchronize { ns_wait_until(timeout) { @count == 0 } }
-
end
-
-
# @!macro count_down_latch_method_count_down
-
1
def count_down
-
synchronize do
-
@count -= 1 if @count > 0
-
ns_broadcast if @count == 0
-
end
-
end
-
-
# @!macro count_down_latch_method_count
-
1
def count
-
synchronize { @count }
-
end
-
-
1
protected
-
-
1
def ns_initialize(count)
-
@count = count
-
end
-
end
-
end
-
1
require 'thread'
-
1
require 'concurrent/atomic/atomic_fixnum'
-
1
require 'concurrent/errors'
-
1
require 'concurrent/synchronization'
-
-
1
module Concurrent
-
-
# Ruby read-write lock implementation
-
#
-
# Allows any number of concurrent readers, but only one concurrent writer
-
# (And if the "write" lock is taken, any readers who come along will have to wait)
-
#
-
# If readers are already active when a writer comes along, the writer will wait for
-
# all the readers to finish before going ahead.
-
# Any additional readers that come when the writer is already waiting, will also
-
# wait (so writers are not starved).
-
#
-
# This implementation is based on `java.util.concurrent.ReentrantReadWriteLock`.
-
#
-
# @example
-
# lock = Concurrent::ReadWriteLock.new
-
# lock.with_read_lock { data.retrieve }
-
# lock.with_write_lock { data.modify! }
-
#
-
# @note Do **not** try to acquire the write lock while already holding a read lock
-
# **or** try to acquire the write lock while you already have it.
-
# This will lead to deadlock
-
#
-
# @see http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/locks/ReentrantReadWriteLock.html java.util.concurrent.ReentrantReadWriteLock
-
1
class ReadWriteLock < Synchronization::Object
-
-
# @!visibility private
-
1
WAITING_WRITER = 1 << 15
-
-
# @!visibility private
-
1
RUNNING_WRITER = 1 << 29
-
-
# @!visibility private
-
1
MAX_READERS = WAITING_WRITER - 1
-
-
# @!visibility private
-
1
MAX_WRITERS = RUNNING_WRITER - MAX_READERS - 1
-
-
1
safe_initialization!
-
-
# Implementation notes:
-
# A goal is to make the uncontended path for both readers/writers lock-free
-
# Only if there is reader-writer or writer-writer contention, should locks be used
-
# Internal state is represented by a single integer ("counter"), and updated
-
# using atomic compare-and-swap operations
-
# When the counter is 0, the lock is free
-
# Each reader increments the counter by 1 when acquiring a read lock
-
# (and decrements by 1 when releasing the read lock)
-
# The counter is increased by (1 << 15) for each writer waiting to acquire the
-
# write lock, and by (1 << 29) if the write lock is taken
-
-
# Create a new `ReadWriteLock` in the unlocked state.
-
1
def initialize
-
super()
-
@Counter = AtomicFixnum.new(0) # single integer which represents lock state
-
@ReadLock = Synchronization::Lock.new
-
@WriteLock = Synchronization::Lock.new
-
end
-
-
# Execute a block operation within a read lock.
-
#
-
# @yield the task to be performed within the lock.
-
#
-
# @return [Object] the result of the block operation.
-
#
-
# @raise [ArgumentError] when no block is given.
-
# @raise [Concurrent::ResourceLimitError] if the maximum number of readers
-
# is exceeded.
-
1
def with_read_lock
-
raise ArgumentError.new('no block given') unless block_given?
-
acquire_read_lock
-
begin
-
yield
-
ensure
-
release_read_lock
-
end
-
end
-
-
# Execute a block operation within a write lock.
-
#
-
# @yield the task to be performed within the lock.
-
#
-
# @return [Object] the result of the block operation.
-
#
-
# @raise [ArgumentError] when no block is given.
-
# @raise [Concurrent::ResourceLimitError] if the maximum number of readers
-
# is exceeded.
-
1
def with_write_lock
-
raise ArgumentError.new('no block given') unless block_given?
-
acquire_write_lock
-
begin
-
yield
-
ensure
-
release_write_lock
-
end
-
end
-
-
# Acquire a read lock. If a write lock has been acquired will block until
-
# it is released. Will not block if other read locks have been acquired.
-
#
-
# @return [Boolean] true if the lock is successfully acquired
-
#
-
# @raise [Concurrent::ResourceLimitError] if the maximum number of readers
-
# is exceeded.
-
1
def acquire_read_lock
-
while true
-
c = @Counter.value
-
raise ResourceLimitError.new('Too many reader threads') if max_readers?(c)
-
-
# If a writer is waiting when we first queue up, we need to wait
-
if waiting_writer?(c)
-
@ReadLock.wait_until { !waiting_writer? }
-
-
# after a reader has waited once, they are allowed to "barge" ahead of waiting writers
-
# but if a writer is *running*, the reader still needs to wait (naturally)
-
while true
-
c = @Counter.value
-
if running_writer?(c)
-
@ReadLock.wait_until { !running_writer? }
-
else
-
return if @Counter.compare_and_set(c, c+1)
-
end
-
end
-
else
-
break if @Counter.compare_and_set(c, c+1)
-
end
-
end
-
true
-
end
-
-
# Release a previously acquired read lock.
-
#
-
# @return [Boolean] true if the lock is successfully released
-
1
def release_read_lock
-
while true
-
c = @Counter.value
-
if @Counter.compare_and_set(c, c-1)
-
# If one or more writers were waiting, and we were the last reader, wake a writer up
-
if waiting_writer?(c) && running_readers(c) == 1
-
@WriteLock.signal
-
end
-
break
-
end
-
end
-
true
-
end
-
-
# Acquire a write lock. Will block and wait for all active readers and writers.
-
#
-
# @return [Boolean] true if the lock is successfully acquired
-
#
-
# @raise [Concurrent::ResourceLimitError] if the maximum number of writers
-
# is exceeded.
-
1
def acquire_write_lock
-
while true
-
c = @Counter.value
-
raise ResourceLimitError.new('Too many writer threads') if max_writers?(c)
-
-
if c == 0 # no readers OR writers running
-
# if we successfully swap the RUNNING_WRITER bit on, then we can go ahead
-
break if @Counter.compare_and_set(0, RUNNING_WRITER)
-
elsif @Counter.compare_and_set(c, c+WAITING_WRITER)
-
while true
-
# Now we have successfully incremented, so no more readers will be able to increment
-
# (they will wait instead)
-
# However, readers OR writers could decrement right here, OR another writer could increment
-
@WriteLock.wait_until do
-
# So we have to do another check inside the synchronized section
-
# If a writer OR reader is running, then go to sleep
-
c = @Counter.value
-
!running_writer?(c) && !running_readers?(c)
-
end
-
-
# We just came out of a wait
-
# If we successfully turn the RUNNING_WRITER bit on with an atomic swap,
-
# Then we are OK to stop waiting and go ahead
-
# Otherwise go back and wait again
-
c = @Counter.value
-
break if !running_writer?(c) && !running_readers?(c) && @Counter.compare_and_set(c, c+RUNNING_WRITER-WAITING_WRITER)
-
end
-
break
-
end
-
end
-
true
-
end
-
-
# Release a previously acquired write lock.
-
#
-
# @return [Boolean] true if the lock is successfully released
-
1
def release_write_lock
-
return true unless running_writer?
-
c = @Counter.update { |counter| counter - RUNNING_WRITER }
-
@ReadLock.broadcast
-
@WriteLock.signal if waiting_writers(c) > 0
-
true
-
end
-
-
# Queries if the write lock is held by any thread.
-
#
-
# @return [Boolean] true if the write lock is held else false`
-
1
def write_locked?
-
@Counter.value >= RUNNING_WRITER
-
end
-
-
# Queries whether any threads are waiting to acquire the read or write lock.
-
#
-
# @return [Boolean] true if any threads are waiting for a lock else false
-
1
def has_waiters?
-
waiting_writer?(@Counter.value)
-
end
-
-
1
private
-
-
# @!visibility private
-
1
def running_readers(c = @Counter.value)
-
c & MAX_READERS
-
end
-
-
# @!visibility private
-
1
def running_readers?(c = @Counter.value)
-
(c & MAX_READERS) > 0
-
end
-
-
# @!visibility private
-
1
def running_writer?(c = @Counter.value)
-
c >= RUNNING_WRITER
-
end
-
-
# @!visibility private
-
1
def waiting_writers(c = @Counter.value)
-
(c & MAX_WRITERS) / WAITING_WRITER
-
end
-
-
# @!visibility private
-
1
def waiting_writer?(c = @Counter.value)
-
c >= WAITING_WRITER
-
end
-
-
# @!visibility private
-
1
def max_readers?(c = @Counter.value)
-
(c & MAX_READERS) == MAX_READERS
-
end
-
-
# @!visibility private
-
1
def max_writers?(c = @Counter.value)
-
(c & MAX_WRITERS) == MAX_WRITERS
-
end
-
end
-
end
-
1
require 'thread'
-
1
require 'concurrent/atomic/atomic_reference'
-
1
require 'concurrent/errors'
-
1
require 'concurrent/synchronization'
-
1
require 'concurrent/atomic/thread_local_var'
-
-
1
module Concurrent
-
-
# Re-entrant read-write lock implementation
-
#
-
# Allows any number of concurrent readers, but only one concurrent writer
-
# (And while the "write" lock is taken, no read locks can be obtained either.
-
# Hence, the write lock can also be called an "exclusive" lock.)
-
#
-
# If another thread has taken a read lock, any thread which wants a write lock
-
# will block until all the readers release their locks. However, once a thread
-
# starts waiting to obtain a write lock, any additional readers that come along
-
# will also wait (so writers are not starved).
-
#
-
# A thread can acquire both a read and write lock at the same time. A thread can
-
# also acquire a read lock OR a write lock more than once. Only when the read (or
-
# write) lock is released as many times as it was acquired, will the thread
-
# actually let it go, allowing other threads which might have been waiting
-
# to proceed. Therefore the lock can be upgraded by first acquiring
-
# read lock and then write lock and that the lock can be downgraded by first
-
# having both read and write lock a releasing just the write lock.
-
#
-
# If both read and write locks are acquired by the same thread, it is not strictly
-
# necessary to release them in the same order they were acquired. In other words,
-
# the following code is legal:
-
#
-
# @example
-
# lock = Concurrent::ReentrantReadWriteLock.new
-
# lock.acquire_write_lock
-
# lock.acquire_read_lock
-
# lock.release_write_lock
-
# # At this point, the current thread is holding only a read lock, not a write
-
# # lock. So other threads can take read locks, but not a write lock.
-
# lock.release_read_lock
-
# # Now the current thread is not holding either a read or write lock, so
-
# # another thread could potentially acquire a write lock.
-
#
-
# This implementation was inspired by `java.util.concurrent.ReentrantReadWriteLock`.
-
#
-
# @example
-
# lock = Concurrent::ReentrantReadWriteLock.new
-
# lock.with_read_lock { data.retrieve }
-
# lock.with_write_lock { data.modify! }
-
#
-
# @see http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/locks/ReentrantReadWriteLock.html java.util.concurrent.ReentrantReadWriteLock
-
1
class ReentrantReadWriteLock < Synchronization::Object
-
-
# Implementation notes:
-
#
-
# A goal is to make the uncontended path for both readers/writers mutex-free
-
# Only if there is reader-writer or writer-writer contention, should mutexes be used
-
# Otherwise, a single CAS operation is all we need to acquire/release a lock
-
#
-
# Internal state is represented by a single integer ("counter"), and updated
-
# using atomic compare-and-swap operations
-
# When the counter is 0, the lock is free
-
# Each thread which has one OR MORE read locks increments the counter by 1
-
# (and decrements by 1 when releasing the read lock)
-
# The counter is increased by (1 << 15) for each writer waiting to acquire the
-
# write lock, and by (1 << 29) if the write lock is taken
-
#
-
# Additionally, each thread uses a thread-local variable to count how many times
-
# it has acquired a read lock, AND how many times it has acquired a write lock.
-
# It uses a similar trick; an increment of 1 means a read lock was taken, and
-
# an increment of (1 << 15) means a write lock was taken
-
# This is what makes re-entrancy possible
-
#
-
# 2 rules are followed to ensure good liveness properties:
-
# 1) Once a writer has queued up and is waiting for a write lock, no other thread
-
# can take a lock without waiting
-
# 2) When a write lock is released, readers are given the "first chance" to wake
-
# up and acquire a read lock
-
# Following these rules means readers and writers tend to "take turns", so neither
-
# can starve the other, even under heavy contention
-
-
# @!visibility private
-
1
READER_BITS = 15
-
# @!visibility private
-
1
WRITER_BITS = 14
-
-
# Used with @Counter:
-
# @!visibility private
-
1
WAITING_WRITER = 1 << READER_BITS
-
# @!visibility private
-
1
RUNNING_WRITER = 1 << (READER_BITS + WRITER_BITS)
-
# @!visibility private
-
1
MAX_READERS = WAITING_WRITER - 1
-
# @!visibility private
-
1
MAX_WRITERS = RUNNING_WRITER - MAX_READERS - 1
-
-
# Used with @HeldCount:
-
# @!visibility private
-
1
WRITE_LOCK_HELD = 1 << READER_BITS
-
# @!visibility private
-
1
READ_LOCK_MASK = WRITE_LOCK_HELD - 1
-
# @!visibility private
-
1
WRITE_LOCK_MASK = MAX_WRITERS
-
-
1
safe_initialization!
-
-
# Create a new `ReentrantReadWriteLock` in the unlocked state.
-
1
def initialize
-
super()
-
@Counter = AtomicFixnum.new(0) # single integer which represents lock state
-
@ReadQueue = Synchronization::Lock.new # used to queue waiting readers
-
@WriteQueue = Synchronization::Lock.new # used to queue waiting writers
-
@HeldCount = ThreadLocalVar.new(0) # indicates # of R & W locks held by this thread
-
end
-
-
# Execute a block operation within a read lock.
-
#
-
# @yield the task to be performed within the lock.
-
#
-
# @return [Object] the result of the block operation.
-
#
-
# @raise [ArgumentError] when no block is given.
-
# @raise [Concurrent::ResourceLimitError] if the maximum number of readers
-
# is exceeded.
-
1
def with_read_lock
-
raise ArgumentError.new('no block given') unless block_given?
-
acquire_read_lock
-
begin
-
yield
-
ensure
-
release_read_lock
-
end
-
end
-
-
# Execute a block operation within a write lock.
-
#
-
# @yield the task to be performed within the lock.
-
#
-
# @return [Object] the result of the block operation.
-
#
-
# @raise [ArgumentError] when no block is given.
-
# @raise [Concurrent::ResourceLimitError] if the maximum number of readers
-
# is exceeded.
-
1
def with_write_lock
-
raise ArgumentError.new('no block given') unless block_given?
-
acquire_write_lock
-
begin
-
yield
-
ensure
-
release_write_lock
-
end
-
end
-
-
# Acquire a read lock. If a write lock is held by another thread, will block
-
# until it is released.
-
#
-
# @return [Boolean] true if the lock is successfully acquired
-
#
-
# @raise [Concurrent::ResourceLimitError] if the maximum number of readers
-
# is exceeded.
-
1
def acquire_read_lock
-
if (held = @HeldCount.value) > 0
-
# If we already have a lock, there's no need to wait
-
if held & READ_LOCK_MASK == 0
-
# But we do need to update the counter, if we were holding a write
-
# lock but not a read lock
-
@Counter.update { |c| c + 1 }
-
end
-
@HeldCount.value = held + 1
-
return true
-
end
-
-
while true
-
c = @Counter.value
-
raise ResourceLimitError.new('Too many reader threads') if max_readers?(c)
-
-
# If a writer is waiting OR running when we first queue up, we need to wait
-
if waiting_or_running_writer?(c)
-
# Before going to sleep, check again with the ReadQueue mutex held
-
@ReadQueue.synchronize do
-
@ReadQueue.ns_wait if waiting_or_running_writer?
-
end
-
# Note: the above 'synchronize' block could have used #wait_until,
-
# but that waits repeatedly in a loop, checking the wait condition
-
# each time it wakes up (to protect against spurious wakeups)
-
# But we are already in a loop, which is only broken when we successfully
-
# acquire the lock! So we don't care about spurious wakeups, and would
-
# rather not pay the extra overhead of using #wait_until
-
-
# After a reader has waited once, they are allowed to "barge" ahead of waiting writers
-
# But if a writer is *running*, the reader still needs to wait (naturally)
-
while true
-
c = @Counter.value
-
if running_writer?(c)
-
@ReadQueue.synchronize do
-
@ReadQueue.ns_wait if running_writer?
-
end
-
elsif @Counter.compare_and_set(c, c+1)
-
@HeldCount.value = held + 1
-
return true
-
end
-
end
-
elsif @Counter.compare_and_set(c, c+1)
-
@HeldCount.value = held + 1
-
return true
-
end
-
end
-
end
-
-
# Try to acquire a read lock and return true if we succeed. If it cannot be
-
# acquired immediately, return false.
-
#
-
# @return [Boolean] true if the lock is successfully acquired
-
1
def try_read_lock
-
if (held = @HeldCount.value) > 0
-
if held & READ_LOCK_MASK == 0
-
# If we hold a write lock, but not a read lock...
-
@Counter.update { |c| c + 1 }
-
end
-
@HeldCount.value = held + 1
-
return true
-
else
-
c = @Counter.value
-
if !waiting_or_running_writer?(c) && @Counter.compare_and_set(c, c+1)
-
@HeldCount.value = held + 1
-
return true
-
end
-
end
-
false
-
end
-
-
# Release a previously acquired read lock.
-
#
-
# @return [Boolean] true if the lock is successfully released
-
1
def release_read_lock
-
held = @HeldCount.value = @HeldCount.value - 1
-
rlocks_held = held & READ_LOCK_MASK
-
if rlocks_held == 0
-
c = @Counter.update { |counter| counter - 1 }
-
# If one or more writers were waiting, and we were the last reader, wake a writer up
-
if waiting_or_running_writer?(c) && running_readers(c) == 0
-
@WriteQueue.signal
-
end
-
elsif rlocks_held == READ_LOCK_MASK
-
raise IllegalOperationError, "Cannot release a read lock which is not held"
-
end
-
true
-
end
-
-
# Acquire a write lock. Will block and wait for all active readers and writers.
-
#
-
# @return [Boolean] true if the lock is successfully acquired
-
#
-
# @raise [Concurrent::ResourceLimitError] if the maximum number of writers
-
# is exceeded.
-
1
def acquire_write_lock
-
if (held = @HeldCount.value) >= WRITE_LOCK_HELD
-
# if we already have a write (exclusive) lock, there's no need to wait
-
@HeldCount.value = held + WRITE_LOCK_HELD
-
return true
-
end
-
-
while true
-
c = @Counter.value
-
raise ResourceLimitError.new('Too many writer threads') if max_writers?(c)
-
-
# To go ahead and take the lock without waiting, there must be no writer
-
# running right now, AND no writers who came before us still waiting to
-
# acquire the lock
-
# Additionally, if any read locks have been taken, we must hold all of them
-
if c == held
-
# If we successfully swap the RUNNING_WRITER bit on, then we can go ahead
-
if @Counter.compare_and_set(c, c+RUNNING_WRITER)
-
@HeldCount.value = held + WRITE_LOCK_HELD
-
return true
-
end
-
elsif @Counter.compare_and_set(c, c+WAITING_WRITER)
-
while true
-
# Now we have successfully incremented, so no more readers will be able to increment
-
# (they will wait instead)
-
# However, readers OR writers could decrement right here
-
@WriteQueue.synchronize do
-
# So we have to do another check inside the synchronized section
-
# If a writer OR another reader is running, then go to sleep
-
c = @Counter.value
-
@WriteQueue.ns_wait if running_writer?(c) || running_readers(c) != held
-
end
-
# Note: if you are thinking of replacing the above 'synchronize' block
-
# with #wait_until, read the comment in #acquire_read_lock first!
-
-
# We just came out of a wait
-
# If we successfully turn the RUNNING_WRITER bit on with an atomic swap,
-
# then we are OK to stop waiting and go ahead
-
# Otherwise go back and wait again
-
c = @Counter.value
-
if !running_writer?(c) &&
-
running_readers(c) == held &&
-
@Counter.compare_and_set(c, c+RUNNING_WRITER-WAITING_WRITER)
-
@HeldCount.value = held + WRITE_LOCK_HELD
-
return true
-
end
-
end
-
end
-
end
-
end
-
-
# Try to acquire a write lock and return true if we succeed. If it cannot be
-
# acquired immediately, return false.
-
#
-
# @return [Boolean] true if the lock is successfully acquired
-
1
def try_write_lock
-
if (held = @HeldCount.value) >= WRITE_LOCK_HELD
-
@HeldCount.value = held + WRITE_LOCK_HELD
-
return true
-
else
-
c = @Counter.value
-
if !waiting_or_running_writer?(c) &&
-
running_readers(c) == held &&
-
@Counter.compare_and_set(c, c+RUNNING_WRITER)
-
@HeldCount.value = held + WRITE_LOCK_HELD
-
return true
-
end
-
end
-
false
-
end
-
-
# Release a previously acquired write lock.
-
#
-
# @return [Boolean] true if the lock is successfully released
-
1
def release_write_lock
-
held = @HeldCount.value = @HeldCount.value - WRITE_LOCK_HELD
-
wlocks_held = held & WRITE_LOCK_MASK
-
if wlocks_held == 0
-
c = @Counter.update { |counter| counter - RUNNING_WRITER }
-
@ReadQueue.broadcast
-
@WriteQueue.signal if waiting_writers(c) > 0
-
elsif wlocks_held == WRITE_LOCK_MASK
-
raise IllegalOperationError, "Cannot release a write lock which is not held"
-
end
-
true
-
end
-
-
1
private
-
-
# @!visibility private
-
1
def running_readers(c = @Counter.value)
-
c & MAX_READERS
-
end
-
-
# @!visibility private
-
1
def running_readers?(c = @Counter.value)
-
(c & MAX_READERS) > 0
-
end
-
-
# @!visibility private
-
1
def running_writer?(c = @Counter.value)
-
c >= RUNNING_WRITER
-
end
-
-
# @!visibility private
-
1
def waiting_writers(c = @Counter.value)
-
(c & MAX_WRITERS) >> READER_BITS
-
end
-
-
# @!visibility private
-
1
def waiting_or_running_writer?(c = @Counter.value)
-
c >= WAITING_WRITER
-
end
-
-
# @!visibility private
-
1
def max_readers?(c = @Counter.value)
-
(c & MAX_READERS) == MAX_READERS
-
end
-
-
# @!visibility private
-
1
def max_writers?(c = @Counter.value)
-
(c & MAX_WRITERS) == MAX_WRITERS
-
end
-
end
-
end
-
1
require 'thread'
-
1
require 'concurrent/atomic/abstract_thread_local_var'
-
-
1
module Concurrent
-
-
# @!visibility private
-
# @!macro internal_implementation_note
-
1
class RubyThreadLocalVar < AbstractThreadLocalVar
-
-
# Each thread has a (lazily initialized) array of thread-local variable values
-
# Each time a new thread-local var is created, we allocate an "index" for it
-
# For example, if the allocated index is 1, that means slot #1 in EVERY
-
# thread's thread-local array will be used for the value of that TLV
-
#
-
# The good thing about using a per-THREAD structure to hold values, rather
-
# than a per-TLV structure, is that no synchronization is needed when
-
# reading and writing those values (since the structure is only ever
-
# accessed by a single thread)
-
#
-
# Of course, when a TLV is GC'd, 1) we need to recover its index for use
-
# by other new TLVs (otherwise the thread-local arrays could get bigger
-
# and bigger with time), and 2) we need to null out all the references
-
# held in the now-unused slots (both to avoid blocking GC of those objects,
-
# and also to prevent "stale" values from being passed on to a new TLV
-
# when the index is reused)
-
# Because we need to null out freed slots, we need to keep references to
-
# ALL the thread-local arrays -- ARRAYS is for that
-
# But when a Thread is GC'd, we need to drop the reference to its thread-local
-
# array, so we don't leak memory
-
-
# @!visibility private
-
1
FREE = []
-
1
LOCK = Mutex.new
-
1
ARRAYS = {} # used as a hash set
-
# noinspection RubyClassVariableUsageInspection
-
1
@@next = 0
-
1
QUEUE = Queue.new
-
1
THREAD = Thread.new do
-
1
while true
-
1
method, i = QUEUE.pop
-
case method
-
when :thread_local_finalizer
-
LOCK.synchronize do
-
FREE.push(i)
-
# The cost of GC'ing a TLV is linear in the number of threads using TLVs
-
# But that is natural! More threads means more storage is used per TLV
-
# So naturally more CPU time is required to free more storage
-
ARRAYS.each_value do |array|
-
array[i] = nil
-
end
-
end
-
when :thread_finalizer
-
LOCK.synchronize do
-
# The thread which used this thread-local array is now gone
-
# So don't hold onto a reference to the array (thus blocking GC)
-
ARRAYS.delete(i)
-
end
-
end
-
end
-
end
-
-
1
private_constant :FREE, :LOCK, :ARRAYS, :QUEUE, :THREAD
-
-
# @!macro thread_local_var_method_get
-
1
def value
-
if (array = get_threadlocal_array)
-
value = array[@index]
-
if value.nil?
-
default
-
elsif value.equal?(NULL)
-
nil
-
else
-
value
-
end
-
else
-
default
-
end
-
end
-
-
# @!macro thread_local_var_method_set
-
1
def value=(value)
-
me = Thread.current
-
# We could keep the thread-local arrays in a hash, keyed by Thread
-
# But why? That would require locking
-
# Using Ruby's built-in thread-local storage is faster
-
unless (array = get_threadlocal_array(me))
-
array = set_threadlocal_array([], me)
-
LOCK.synchronize { ARRAYS[array.object_id] = array }
-
ObjectSpace.define_finalizer(me, self.class.thread_finalizer(array.object_id))
-
end
-
array[@index] = (value.nil? ? NULL : value)
-
value
-
end
-
-
1
protected
-
-
# @!visibility private
-
# noinspection RubyClassVariableUsageInspection
-
1
def allocate_storage
-
@index = LOCK.synchronize do
-
FREE.pop || begin
-
result = @@next
-
@@next += 1
-
result
-
end
-
end
-
ObjectSpace.define_finalizer(self, self.class.thread_local_finalizer(@index))
-
end
-
-
# @!visibility private
-
1
def self.thread_local_finalizer(index)
-
# avoid error: can't be called from trap context
-
proc { QUEUE.push [:thread_local_finalizer, index] }
-
end
-
-
# @!visibility private
-
1
def self.thread_finalizer(id)
-
# avoid error: can't be called from trap context
-
proc { QUEUE.push [:thread_finalizer, id] }
-
end
-
-
1
private
-
-
1
if Thread.instance_methods.include?(:thread_variable_get)
-
-
1
def get_threadlocal_array(thread = Thread.current)
-
thread.thread_variable_get(:__threadlocal_array__)
-
end
-
-
1
def set_threadlocal_array(array, thread = Thread.current)
-
thread.thread_variable_set(:__threadlocal_array__, array)
-
end
-
-
else
-
-
def get_threadlocal_array(thread = Thread.current)
-
thread[:__threadlocal_array__]
-
end
-
-
def set_threadlocal_array(array, thread = Thread.current)
-
thread[:__threadlocal_array__] = array
-
end
-
end
-
-
# This exists only for use in testing
-
# @!visibility private
-
1
def value_for(thread)
-
if (array = get_threadlocal_array(thread))
-
value = array[@index]
-
if value.nil?
-
get_default
-
elsif value.equal?(NULL)
-
nil
-
else
-
value
-
end
-
else
-
get_default
-
end
-
end
-
-
# @!visibility private
-
1
def get_default
-
if @default_block
-
raise "Cannot use default_for with default block"
-
else
-
@default
-
end
-
end
-
end
-
end
-
1
require 'concurrent/atomic/mutex_semaphore'
-
1
require 'concurrent/synchronization'
-
-
1
module Concurrent
-
-
###################################################################
-
-
# @!macro semaphore_method_initialize
-
#
-
# Create a new `Semaphore` with the initial `count`.
-
#
-
# @param [Fixnum] count the initial count
-
#
-
# @raise [ArgumentError] if `count` is not an integer or is less than zero
-
-
# @!macro semaphore_method_acquire
-
#
-
# Acquires the given number of permits from this semaphore,
-
# blocking until all are available.
-
#
-
# @param [Fixnum] permits Number of permits to acquire
-
#
-
# @raise [ArgumentError] if `permits` is not an integer or is less than
-
# one
-
#
-
# @return [nil]
-
-
# @!macro semaphore_method_available_permits
-
#
-
# Returns the current number of permits available in this semaphore.
-
#
-
# @return [Integer]
-
-
# @!macro semaphore_method_drain_permits
-
#
-
# Acquires and returns all permits that are immediately available.
-
#
-
# @return [Integer]
-
-
# @!macro semaphore_method_try_acquire
-
#
-
# Acquires the given number of permits from this semaphore,
-
# only if all are available at the time of invocation or within
-
# `timeout` interval
-
#
-
# @param [Fixnum] permits the number of permits to acquire
-
#
-
# @param [Fixnum] timeout the number of seconds to wait for the counter
-
# or `nil` to return immediately
-
#
-
# @raise [ArgumentError] if `permits` is not an integer or is less than
-
# one
-
#
-
# @return [Boolean] `false` if no permits are available, `true` when
-
# acquired a permit
-
-
# @!macro semaphore_method_release
-
#
-
# Releases the given number of permits, returning them to the semaphore.
-
#
-
# @param [Fixnum] permits Number of permits to return to the semaphore.
-
#
-
# @raise [ArgumentError] if `permits` is not a number or is less than one
-
#
-
# @return [nil]
-
-
###################################################################
-
-
# @!macro semaphore_public_api
-
#
-
# @!method initialize(count)
-
# @!macro semaphore_method_initialize
-
#
-
# @!method acquire(permits = 1)
-
# @!macro semaphore_method_acquire
-
#
-
# @!method available_permits
-
# @!macro semaphore_method_available_permits
-
#
-
# @!method drain_permits
-
# @!macro semaphore_method_drain_permits
-
#
-
# @!method try_acquire(permits = 1, timeout = nil)
-
# @!macro semaphore_method_try_acquire
-
#
-
# @!method release(permits = 1)
-
# @!macro semaphore_method_release
-
-
###################################################################
-
-
# @!visibility private
-
# @!macro internal_implementation_note
-
SemaphoreImplementation = case
-
1
when defined?(JavaSemaphore)
-
JavaSemaphore
-
else
-
1
MutexSemaphore
-
end
-
1
private_constant :SemaphoreImplementation
-
-
# @!macro semaphore
-
#
-
# A counting semaphore. Conceptually, a semaphore maintains a set of
-
# permits. Each {#acquire} blocks if necessary until a permit is
-
# available, and then takes it. Each {#release} adds a permit, potentially
-
# releasing a blocking acquirer.
-
# However, no actual permit objects are used; the Semaphore just keeps a
-
# count of the number available and acts accordingly.
-
#
-
# @!macro semaphore_public_api
-
# @example
-
# semaphore = Concurrent::Semaphore.new(2)
-
#
-
# t1 = Thread.new do
-
# semaphore.acquire
-
# puts "Thread 1 acquired semaphore"
-
# end
-
#
-
# t2 = Thread.new do
-
# semaphore.acquire
-
# puts "Thread 2 acquired semaphore"
-
# end
-
#
-
# t3 = Thread.new do
-
# semaphore.acquire
-
# puts "Thread 3 acquired semaphore"
-
# end
-
#
-
# t4 = Thread.new do
-
# sleep(2)
-
# puts "Thread 4 releasing semaphore"
-
# semaphore.release
-
# end
-
#
-
# [t1, t2, t3, t4].each(&:join)
-
#
-
# # prints:
-
# # Thread 3 acquired semaphore
-
# # Thread 2 acquired semaphore
-
# # Thread 4 releasing semaphore
-
# # Thread 1 acquired semaphore
-
#
-
1
class Semaphore < SemaphoreImplementation
-
end
-
end
-
1
module Concurrent
-
-
# @!visibility private
-
# @!macro internal_implementation_note
-
1
class MutexAtomicReference < Synchronization::LockableObject
-
1
include AtomicDirectUpdate
-
1
include AtomicNumericCompareAndSetWrapper
-
1
alias_method :compare_and_swap, :compare_and_set
-
-
# @!macro atomic_reference_method_initialize
-
1
def initialize(value = nil)
-
1
super()
-
2
synchronize { ns_initialize(value) }
-
end
-
-
# @!macro atomic_reference_method_get
-
1
def get
-
synchronize { @value }
-
end
-
1
alias_method :value, :get
-
-
# @!macro atomic_reference_method_set
-
1
def set(new_value)
-
synchronize { @value = new_value }
-
end
-
1
alias_method :value=, :set
-
-
# @!macro atomic_reference_method_get_and_set
-
1
def get_and_set(new_value)
-
synchronize do
-
old_value = @value
-
@value = new_value
-
old_value
-
end
-
end
-
1
alias_method :swap, :get_and_set
-
-
# @!macro atomic_reference_method_compare_and_set
-
1
def _compare_and_set(old_value, new_value)
-
synchronize do
-
if @value.equal? old_value
-
@value = new_value
-
true
-
else
-
false
-
end
-
end
-
end
-
-
1
protected
-
-
1
def ns_initialize(value)
-
1
@value = value
-
end
-
end
-
end
-
1
module Concurrent
-
-
# Special "compare and set" handling of numeric values.
-
#
-
# @!visibility private
-
# @!macro internal_implementation_note
-
1
module AtomicNumericCompareAndSetWrapper
-
-
# @!macro atomic_reference_method_compare_and_set
-
1
def compare_and_set(old_value, new_value)
-
if old_value.kind_of? Numeric
-
while true
-
old = get
-
-
return false unless old.kind_of? Numeric
-
-
return false unless old == old_value
-
-
result = _compare_and_set(old, new_value)
-
return result if result
-
end
-
else
-
_compare_and_set(old_value, new_value)
-
end
-
end
-
-
end
-
end
-
1
require 'concurrent/atomic/atomic_reference'
-
1
require 'concurrent/atomic/atomic_boolean'
-
1
require 'concurrent/atomic/atomic_fixnum'
-
1
require 'concurrent/atomic/cyclic_barrier'
-
1
require 'concurrent/atomic/count_down_latch'
-
1
require 'concurrent/atomic/event'
-
1
require 'concurrent/atomic/read_write_lock'
-
1
require 'concurrent/atomic/reentrant_read_write_lock'
-
1
require 'concurrent/atomic/semaphore'
-
1
require 'concurrent/atomic/thread_local_var'
-
1
require 'concurrent/synchronization'
-
-
1
module Concurrent
-
1
module Collection
-
-
# A thread safe observer set implemented using copy-on-read approach:
-
# observers are added and removed from a thread safe collection; every time
-
# a notification is required the internal data structure is copied to
-
# prevent concurrency issues
-
#
-
# @api private
-
1
class CopyOnNotifyObserverSet < Synchronization::LockableObject
-
-
1
def initialize
-
super()
-
synchronize { ns_initialize }
-
end
-
-
# @!macro observable_add_observer
-
1
def add_observer(observer = nil, func = :update, &block)
-
if observer.nil? && block.nil?
-
raise ArgumentError, 'should pass observer as a first argument or block'
-
elsif observer && block
-
raise ArgumentError.new('cannot provide both an observer and a block')
-
end
-
-
if block
-
observer = block
-
func = :call
-
end
-
-
synchronize do
-
@observers[observer] = func
-
observer
-
end
-
end
-
-
# @!macro observable_delete_observer
-
1
def delete_observer(observer)
-
synchronize do
-
@observers.delete(observer)
-
observer
-
end
-
end
-
-
# @!macro observable_delete_observers
-
1
def delete_observers
-
synchronize do
-
@observers.clear
-
self
-
end
-
end
-
-
# @!macro observable_count_observers
-
1
def count_observers
-
synchronize { @observers.count }
-
end
-
-
# Notifies all registered observers with optional args
-
# @param [Object] args arguments to be passed to each observer
-
# @return [CopyOnWriteObserverSet] self
-
1
def notify_observers(*args, &block)
-
observers = duplicate_observers
-
notify_to(observers, *args, &block)
-
self
-
end
-
-
# Notifies all registered observers with optional args and deletes them.
-
#
-
# @param [Object] args arguments to be passed to each observer
-
# @return [CopyOnWriteObserverSet] self
-
1
def notify_and_delete_observers(*args, &block)
-
observers = duplicate_and_clear_observers
-
notify_to(observers, *args, &block)
-
self
-
end
-
-
1
protected
-
-
1
def ns_initialize
-
@observers = {}
-
end
-
-
1
private
-
-
1
def duplicate_and_clear_observers
-
synchronize do
-
observers = @observers.dup
-
@observers.clear
-
observers
-
end
-
end
-
-
1
def duplicate_observers
-
synchronize { @observers.dup }
-
end
-
-
1
def notify_to(observers, *args)
-
raise ArgumentError.new('cannot give arguments and a block') if block_given? && !args.empty?
-
observers.each do |observer, function|
-
args = yield if block_given?
-
observer.send(function, *args)
-
end
-
end
-
end
-
end
-
end
-
1
require 'concurrent/synchronization'
-
-
1
module Concurrent
-
1
module Collection
-
-
# A thread safe observer set implemented using copy-on-write approach:
-
# every time an observer is added or removed the whole internal data structure is
-
# duplicated and replaced with a new one.
-
#
-
# @api private
-
1
class CopyOnWriteObserverSet < Synchronization::LockableObject
-
-
1
def initialize
-
super()
-
synchronize { ns_initialize }
-
end
-
-
# @!macro observable_add_observer
-
1
def add_observer(observer = nil, func = :update, &block)
-
if observer.nil? && block.nil?
-
raise ArgumentError, 'should pass observer as a first argument or block'
-
elsif observer && block
-
raise ArgumentError.new('cannot provide both an observer and a block')
-
end
-
-
if block
-
observer = block
-
func = :call
-
end
-
-
synchronize do
-
new_observers = @observers.dup
-
new_observers[observer] = func
-
@observers = new_observers
-
observer
-
end
-
end
-
-
# @!macro observable_delete_observer
-
1
def delete_observer(observer)
-
synchronize do
-
new_observers = @observers.dup
-
new_observers.delete(observer)
-
@observers = new_observers
-
observer
-
end
-
end
-
-
# @!macro observable_delete_observers
-
1
def delete_observers
-
self.observers = {}
-
self
-
end
-
-
# @!macro observable_count_observers
-
1
def count_observers
-
observers.count
-
end
-
-
# Notifies all registered observers with optional args
-
# @param [Object] args arguments to be passed to each observer
-
# @return [CopyOnWriteObserverSet] self
-
1
def notify_observers(*args, &block)
-
notify_to(observers, *args, &block)
-
self
-
end
-
-
# Notifies all registered observers with optional args and deletes them.
-
#
-
# @param [Object] args arguments to be passed to each observer
-
# @return [CopyOnWriteObserverSet] self
-
1
def notify_and_delete_observers(*args, &block)
-
old = clear_observers_and_return_old
-
notify_to(old, *args, &block)
-
self
-
end
-
-
1
protected
-
-
1
def ns_initialize
-
@observers = {}
-
end
-
-
1
private
-
-
1
def notify_to(observers, *args)
-
raise ArgumentError.new('cannot give arguments and a block') if block_given? && !args.empty?
-
observers.each do |observer, function|
-
args = yield if block_given?
-
observer.send(function, *args)
-
end
-
end
-
-
1
def observers
-
synchronize { @observers }
-
end
-
-
1
def observers=(new_set)
-
synchronize { @observers = new_set }
-
end
-
-
1
def clear_observers_and_return_old
-
synchronize do
-
old_observers = @observers
-
@observers = {}
-
old_observers
-
end
-
end
-
end
-
end
-
end
-
1
if Concurrent.on_jruby?
-
-
module Concurrent
-
module Collection
-
-
-
# @!macro priority_queue
-
#
-
# @!visibility private
-
# @!macro internal_implementation_note
-
class JavaNonConcurrentPriorityQueue
-
-
# @!macro priority_queue_method_initialize
-
def initialize(opts = {})
-
order = opts.fetch(:order, :max)
-
if [:min, :low].include?(order)
-
@queue = java.util.PriorityQueue.new(11) # 11 is the default initial capacity
-
else
-
@queue = java.util.PriorityQueue.new(11, java.util.Collections.reverseOrder())
-
end
-
end
-
-
# @!macro priority_queue_method_clear
-
def clear
-
@queue.clear
-
true
-
end
-
-
# @!macro priority_queue_method_delete
-
def delete(item)
-
found = false
-
while @queue.remove(item) do
-
found = true
-
end
-
found
-
end
-
-
# @!macro priority_queue_method_empty
-
def empty?
-
@queue.size == 0
-
end
-
-
# @!macro priority_queue_method_include
-
def include?(item)
-
@queue.contains(item)
-
end
-
alias_method :has_priority?, :include?
-
-
# @!macro priority_queue_method_length
-
def length
-
@queue.size
-
end
-
alias_method :size, :length
-
-
# @!macro priority_queue_method_peek
-
def peek
-
@queue.peek
-
end
-
-
# @!macro priority_queue_method_pop
-
def pop
-
@queue.poll
-
end
-
alias_method :deq, :pop
-
alias_method :shift, :pop
-
-
# @!macro priority_queue_method_push
-
def push(item)
-
raise ArgumentError.new('cannot enqueue nil') if item.nil?
-
@queue.add(item)
-
end
-
alias_method :<<, :push
-
alias_method :enq, :push
-
-
# @!macro priority_queue_method_from_list
-
def self.from_list(list, opts = {})
-
queue = new(opts)
-
list.each{|item| queue << item }
-
queue
-
end
-
end
-
end
-
end
-
end
-
1
module Concurrent
-
-
# @!macro warn.edge
-
1
class LockFreeStack < Synchronization::Object
-
-
1
safe_initialization!
-
-
1
class Node
-
# TODO (pitr-ch 20-Dec-2016): Could be unified with Stack class?
-
-
# @return [Node]
-
1
attr_reader :next_node
-
-
# @return [Object]
-
1
attr_reader :value
-
-
# @!visibility private
-
# allow to nil-ify to free GC when the entry is no longer relevant, not synchronised
-
1
attr_writer :value
-
-
1
def initialize(value, next_node)
-
1
@value = value
-
1
@next_node = next_node
-
end
-
-
1
singleton_class.send :alias_method, :[], :new
-
end
-
-
# The singleton for empty node
-
1
EMPTY = Node[nil, nil]
-
1
def EMPTY.next_node
-
self
-
end
-
-
1
attr_atomic(:head)
-
1
private :head, :head=, :swap_head, :compare_and_set_head, :update_head
-
-
# @!visibility private
-
1
def self.of1(value)
-
new Node[value, EMPTY]
-
end
-
-
# @!visibility private
-
1
def self.of2(value1, value2)
-
new Node[value1, Node[value2, EMPTY]]
-
end
-
-
# @param [Node] head
-
1
def initialize(head = EMPTY)
-
super()
-
self.head = head
-
end
-
-
# @param [Node] head
-
# @return [true, false]
-
1
def empty?(head = head())
-
head.equal? EMPTY
-
end
-
-
# @param [Node] head
-
# @param [Object] value
-
# @return [true, false]
-
1
def compare_and_push(head, value)
-
compare_and_set_head head, Node[value, head]
-
end
-
-
# @param [Object] value
-
# @return [self]
-
1
def push(value)
-
while true
-
current_head = head
-
return self if compare_and_set_head current_head, Node[value, current_head]
-
end
-
end
-
-
# @return [Node]
-
1
def peek
-
head
-
end
-
-
# @param [Node] head
-
# @return [true, false]
-
1
def compare_and_pop(head)
-
compare_and_set_head head, head.next_node
-
end
-
-
# @return [Object]
-
1
def pop
-
while true
-
current_head = head
-
return current_head.value if compare_and_set_head current_head, current_head.next_node
-
end
-
end
-
-
# @param [Node] head
-
# @return [true, false]
-
1
def compare_and_clear(head)
-
compare_and_set_head head, EMPTY
-
end
-
-
1
include Enumerable
-
-
# @param [Node] head
-
# @return [self]
-
1
def each(head = nil)
-
return to_enum(:each, head) unless block_given?
-
it = head || peek
-
until it.equal?(EMPTY)
-
yield it.value
-
it = it.next_node
-
end
-
self
-
end
-
-
# @return [true, false]
-
1
def clear
-
while true
-
current_head = head
-
return false if current_head == EMPTY
-
return true if compare_and_set_head current_head, EMPTY
-
end
-
end
-
-
# @param [Node] head
-
# @return [true, false]
-
1
def clear_if(head)
-
compare_and_set_head head, EMPTY
-
end
-
-
# @param [Node] head
-
# @param [Node] new_head
-
# @return [true, false]
-
1
def replace_if(head, new_head)
-
compare_and_set_head head, new_head
-
end
-
-
# @return [self]
-
# @yield over the cleared stack
-
# @yieldparam [Object] value
-
1
def clear_each(&block)
-
while true
-
current_head = head
-
return self if current_head == EMPTY
-
if compare_and_set_head current_head, EMPTY
-
each current_head, &block
-
return self
-
end
-
end
-
end
-
-
# @return [String] Short string representation.
-
1
def to_s
-
format '%s %s>', super[0..-2], to_a.to_s
-
end
-
-
1
alias_method :inspect, :to_s
-
end
-
end
-
1
require 'thread'
-
1
require 'concurrent/collection/map/non_concurrent_map_backend'
-
-
1
module Concurrent
-
-
# @!visibility private
-
1
module Collection
-
-
# @!visibility private
-
1
class MriMapBackend < NonConcurrentMapBackend
-
-
1
def initialize(options = nil)
-
60
super(options)
-
60
@write_lock = Mutex.new
-
end
-
-
1
def []=(key, value)
-
@write_lock.synchronize { super }
-
end
-
-
1
def compute_if_absent(key)
-
if stored_value = _get(key) # fast non-blocking path for the most likely case
-
stored_value
-
else
-
@write_lock.synchronize { super }
-
end
-
end
-
-
1
def compute_if_present(key)
-
@write_lock.synchronize { super }
-
end
-
-
1
def compute(key)
-
@write_lock.synchronize { super }
-
end
-
-
1
def merge_pair(key, value)
-
@write_lock.synchronize { super }
-
end
-
-
1
def replace_pair(key, old_value, new_value)
-
@write_lock.synchronize { super }
-
end
-
-
1
def replace_if_exists(key, new_value)
-
@write_lock.synchronize { super }
-
end
-
-
1
def get_and_set(key, value)
-
@write_lock.synchronize { super }
-
end
-
-
1
def delete(key)
-
@write_lock.synchronize { super }
-
end
-
-
1
def delete_pair(key, value)
-
@write_lock.synchronize { super }
-
end
-
-
1
def clear
-
@write_lock.synchronize { super }
-
end
-
end
-
end
-
end
-
1
require 'concurrent/constants'
-
-
1
module Concurrent
-
-
# @!visibility private
-
1
module Collection
-
-
# @!visibility private
-
1
class NonConcurrentMapBackend
-
-
# WARNING: all public methods of the class must operate on the @backend
-
# directly without calling each other. This is important because of the
-
# SynchronizedMapBackend which uses a non-reentrant mutex for performance
-
# reasons.
-
1
def initialize(options = nil)
-
60
@backend = {}
-
end
-
-
1
def [](key)
-
@backend[key]
-
end
-
-
1
def []=(key, value)
-
@backend[key] = value
-
end
-
-
1
def compute_if_absent(key)
-
if NULL != (stored_value = @backend.fetch(key, NULL))
-
stored_value
-
else
-
@backend[key] = yield
-
end
-
end
-
-
1
def replace_pair(key, old_value, new_value)
-
if pair?(key, old_value)
-
@backend[key] = new_value
-
true
-
else
-
false
-
end
-
end
-
-
1
def replace_if_exists(key, new_value)
-
if NULL != (stored_value = @backend.fetch(key, NULL))
-
@backend[key] = new_value
-
stored_value
-
end
-
end
-
-
1
def compute_if_present(key)
-
if NULL != (stored_value = @backend.fetch(key, NULL))
-
store_computed_value(key, yield(stored_value))
-
end
-
end
-
-
1
def compute(key)
-
store_computed_value(key, yield(@backend[key]))
-
end
-
-
1
def merge_pair(key, value)
-
if NULL == (stored_value = @backend.fetch(key, NULL))
-
@backend[key] = value
-
else
-
store_computed_value(key, yield(stored_value))
-
end
-
end
-
-
1
def get_and_set(key, value)
-
stored_value = @backend[key]
-
@backend[key] = value
-
stored_value
-
end
-
-
1
def key?(key)
-
@backend.key?(key)
-
end
-
-
1
def delete(key)
-
@backend.delete(key)
-
end
-
-
1
def delete_pair(key, value)
-
if pair?(key, value)
-
@backend.delete(key)
-
true
-
else
-
false
-
end
-
end
-
-
1
def clear
-
@backend.clear
-
self
-
end
-
-
1
def each_pair
-
dupped_backend.each_pair do |k, v|
-
yield k, v
-
end
-
self
-
end
-
-
1
def size
-
@backend.size
-
end
-
-
1
def get_or_default(key, default_value)
-
@backend.fetch(key, default_value)
-
end
-
-
1
alias_method :_get, :[]
-
1
alias_method :_set, :[]=
-
1
private :_get, :_set
-
1
private
-
1
def initialize_copy(other)
-
super
-
@backend = {}
-
self
-
end
-
-
1
def dupped_backend
-
@backend.dup
-
end
-
-
1
def pair?(key, expected_value)
-
NULL != (stored_value = @backend.fetch(key, NULL)) && expected_value.equal?(stored_value)
-
end
-
-
1
def store_computed_value(key, new_value)
-
if new_value.nil?
-
@backend.delete(key)
-
nil
-
else
-
@backend[key] = new_value
-
end
-
end
-
end
-
end
-
end
-
1
require 'concurrent/utility/engine'
-
1
require 'concurrent/collection/java_non_concurrent_priority_queue'
-
1
require 'concurrent/collection/ruby_non_concurrent_priority_queue'
-
-
1
module Concurrent
-
1
module Collection
-
-
# @!visibility private
-
# @!macro internal_implementation_note
-
NonConcurrentPriorityQueueImplementation = case
-
1
when Concurrent.on_jruby?
-
JavaNonConcurrentPriorityQueue
-
else
-
1
RubyNonConcurrentPriorityQueue
-
end
-
1
private_constant :NonConcurrentPriorityQueueImplementation
-
-
# @!macro priority_queue
-
#
-
# A queue collection in which the elements are sorted based on their
-
# comparison (spaceship) operator `<=>`. Items are added to the queue
-
# at a position relative to their priority. On removal the element
-
# with the "highest" priority is removed. By default the sort order is
-
# from highest to lowest, but a lowest-to-highest sort order can be
-
# set on construction.
-
#
-
# The API is based on the `Queue` class from the Ruby standard library.
-
#
-
# The pure Ruby implementation, `RubyNonConcurrentPriorityQueue` uses a heap algorithm
-
# stored in an array. The algorithm is based on the work of Robert Sedgewick
-
# and Kevin Wayne.
-
#
-
# The JRuby native implementation is a thin wrapper around the standard
-
# library `java.util.NonConcurrentPriorityQueue`.
-
#
-
# When running under JRuby the class `NonConcurrentPriorityQueue` extends `JavaNonConcurrentPriorityQueue`.
-
# When running under all other interpreters it extends `RubyNonConcurrentPriorityQueue`.
-
#
-
# @note This implementation is *not* thread safe.
-
#
-
# @see http://en.wikipedia.org/wiki/Priority_queue
-
# @see http://ruby-doc.org/stdlib-2.0.0/libdoc/thread/rdoc/Queue.html
-
#
-
# @see http://algs4.cs.princeton.edu/24pq/index.php#2.6
-
# @see http://algs4.cs.princeton.edu/24pq/MaxPQ.java.html
-
#
-
# @see http://docs.oracle.com/javase/7/docs/api/java/util/PriorityQueue.html
-
#
-
# @!visibility private
-
1
class NonConcurrentPriorityQueue < NonConcurrentPriorityQueueImplementation
-
-
1
alias_method :has_priority?, :include?
-
-
1
alias_method :size, :length
-
-
1
alias_method :deq, :pop
-
1
alias_method :shift, :pop
-
-
1
alias_method :<<, :push
-
1
alias_method :enq, :push
-
-
# @!method initialize(opts = {})
-
# @!macro priority_queue_method_initialize
-
#
-
# Create a new priority queue with no items.
-
#
-
# @param [Hash] opts the options for creating the queue
-
# @option opts [Symbol] :order (:max) dictates the order in which items are
-
# stored: from highest to lowest when `:max` or `:high`; from lowest to
-
# highest when `:min` or `:low`
-
-
# @!method clear
-
# @!macro priority_queue_method_clear
-
#
-
# Removes all of the elements from this priority queue.
-
-
# @!method delete(item)
-
# @!macro priority_queue_method_delete
-
#
-
# Deletes all items from `self` that are equal to `item`.
-
#
-
# @param [Object] item the item to be removed from the queue
-
# @return [Object] true if the item is found else false
-
-
# @!method empty?
-
# @!macro priority_queue_method_empty
-
#
-
# Returns `true` if `self` contains no elements.
-
#
-
# @return [Boolean] true if there are no items in the queue else false
-
-
# @!method include?(item)
-
# @!macro priority_queue_method_include
-
#
-
# Returns `true` if the given item is present in `self` (that is, if any
-
# element == `item`), otherwise returns false.
-
#
-
# @param [Object] item the item to search for
-
#
-
# @return [Boolean] true if the item is found else false
-
-
# @!method length
-
# @!macro priority_queue_method_length
-
#
-
# The current length of the queue.
-
#
-
# @return [Fixnum] the number of items in the queue
-
-
# @!method peek
-
# @!macro priority_queue_method_peek
-
#
-
# Retrieves, but does not remove, the head of this queue, or returns `nil`
-
# if this queue is empty.
-
#
-
# @return [Object] the head of the queue or `nil` when empty
-
-
# @!method pop
-
# @!macro priority_queue_method_pop
-
#
-
# Retrieves and removes the head of this queue, or returns `nil` if this
-
# queue is empty.
-
#
-
# @return [Object] the head of the queue or `nil` when empty
-
-
# @!method push(item)
-
# @!macro priority_queue_method_push
-
#
-
# Inserts the specified element into this priority queue.
-
#
-
# @param [Object] item the item to insert onto the queue
-
-
# @!method self.from_list(list, opts = {})
-
# @!macro priority_queue_method_from_list
-
#
-
# Create a new priority queue from the given list.
-
#
-
# @param [Enumerable] list the list to build the queue from
-
# @param [Hash] opts the options for creating the queue
-
#
-
# @return [NonConcurrentPriorityQueue] the newly created and populated queue
-
end
-
end
-
end
-
1
module Concurrent
-
1
module Collection
-
-
# @!macro priority_queue
-
#
-
# @!visibility private
-
# @!macro internal_implementation_note
-
1
class RubyNonConcurrentPriorityQueue
-
-
# @!macro priority_queue_method_initialize
-
1
def initialize(opts = {})
-
order = opts.fetch(:order, :max)
-
@comparator = [:min, :low].include?(order) ? -1 : 1
-
clear
-
end
-
-
# @!macro priority_queue_method_clear
-
1
def clear
-
@queue = [nil]
-
@length = 0
-
true
-
end
-
-
# @!macro priority_queue_method_delete
-
1
def delete(item)
-
return false if empty?
-
original_length = @length
-
k = 1
-
while k <= @length
-
if @queue[k] == item
-
swap(k, @length)
-
@length -= 1
-
sink(k)
-
@queue.pop
-
else
-
k += 1
-
end
-
end
-
@length != original_length
-
end
-
-
# @!macro priority_queue_method_empty
-
1
def empty?
-
size == 0
-
end
-
-
# @!macro priority_queue_method_include
-
1
def include?(item)
-
@queue.include?(item)
-
end
-
1
alias_method :has_priority?, :include?
-
-
# @!macro priority_queue_method_length
-
1
def length
-
@length
-
end
-
1
alias_method :size, :length
-
-
# @!macro priority_queue_method_peek
-
1
def peek
-
empty? ? nil : @queue[1]
-
end
-
-
# @!macro priority_queue_method_pop
-
1
def pop
-
return nil if empty?
-
max = @queue[1]
-
swap(1, @length)
-
@length -= 1
-
sink(1)
-
@queue.pop
-
max
-
end
-
1
alias_method :deq, :pop
-
1
alias_method :shift, :pop
-
-
# @!macro priority_queue_method_push
-
1
def push(item)
-
raise ArgumentError.new('cannot enqueue nil') if item.nil?
-
@length += 1
-
@queue << item
-
swim(@length)
-
true
-
end
-
1
alias_method :<<, :push
-
1
alias_method :enq, :push
-
-
# @!macro priority_queue_method_from_list
-
1
def self.from_list(list, opts = {})
-
queue = new(opts)
-
list.each{|item| queue << item }
-
queue
-
end
-
-
1
private
-
-
# Exchange the values at the given indexes within the internal array.
-
#
-
# @param [Integer] x the first index to swap
-
# @param [Integer] y the second index to swap
-
#
-
# @!visibility private
-
1
def swap(x, y)
-
temp = @queue[x]
-
@queue[x] = @queue[y]
-
@queue[y] = temp
-
end
-
-
# Are the items at the given indexes ordered based on the priority
-
# order specified at construction?
-
#
-
# @param [Integer] x the first index from which to retrieve a comparable value
-
# @param [Integer] y the second index from which to retrieve a comparable value
-
#
-
# @return [Boolean] true if the two elements are in the correct priority order
-
# else false
-
#
-
# @!visibility private
-
1
def ordered?(x, y)
-
(@queue[x] <=> @queue[y]) == @comparator
-
end
-
-
# Percolate down to maintain heap invariant.
-
#
-
# @param [Integer] k the index at which to start the percolation
-
#
-
# @!visibility private
-
1
def sink(k)
-
while (j = (2 * k)) <= @length do
-
j += 1 if j < @length && ! ordered?(j, j+1)
-
break if ordered?(k, j)
-
swap(k, j)
-
k = j
-
end
-
end
-
-
# Percolate up to maintain heap invariant.
-
#
-
# @param [Integer] k the index at which to start the percolation
-
#
-
# @!visibility private
-
1
def swim(k)
-
while k > 1 && ! ordered?(k/2, k) do
-
swap(k, k/2)
-
k = k/2
-
end
-
end
-
end
-
end
-
end
-
1
require 'concurrent/concern/logging'
-
-
1
module Concurrent
-
1
module Concern
-
-
# @!visibility private
-
# @!macro internal_implementation_note
-
1
module Deprecation
-
# TODO require additional parameter: a version. Display when it'll be removed based on that. Error if not removed.
-
1
include Concern::Logging
-
-
1
def deprecated(message, strip = 2)
-
caller_line = caller(strip).first if strip > 0
-
klass = if Module === self
-
self
-
else
-
self.class
-
end
-
message = if strip > 0
-
format("[DEPRECATED] %s\ncalled on: %s", message, caller_line)
-
else
-
format('[DEPRECATED] %s', message)
-
end
-
log WARN, klass.to_s, message
-
end
-
-
1
def deprecated_method(old_name, new_name)
-
deprecated "`#{old_name}` is deprecated and it'll removed in next release, use `#{new_name}` instead", 3
-
end
-
-
1
extend self
-
end
-
end
-
end
-
1
module Concurrent
-
1
module Concern
-
-
# Object references in Ruby are mutable. This can lead to serious problems when
-
# the `#value` of a concurrent object is a mutable reference. Which is always the
-
# case unless the value is a `Fixnum`, `Symbol`, or similar "primitive" data type.
-
# Most classes in this library that expose a `#value` getter method do so using the
-
# `Dereferenceable` mixin module.
-
#
-
# @!macro copy_options
-
1
module Dereferenceable
-
# NOTE: This module is going away in 2.0. In the mean time we need it to
-
# play nicely with the synchronization layer. This means that the
-
# including class SHOULD be synchronized and it MUST implement a
-
# `#synchronize` method. Not doing so will lead to runtime errors.
-
-
# Return the value this object represents after applying the options specified
-
# by the `#set_deref_options` method.
-
#
-
# @return [Object] the current value of the object
-
1
def value
-
synchronize { apply_deref_options(@value) }
-
end
-
1
alias_method :deref, :value
-
-
1
protected
-
-
# Set the internal value of this object
-
#
-
# @param [Object] value the new value
-
1
def value=(value)
-
synchronize{ @value = value }
-
end
-
-
# @!macro dereferenceable_set_deref_options
-
# Set the options which define the operations #value performs before
-
# returning data to the caller (dereferencing).
-
#
-
# @note Most classes that include this module will call `#set_deref_options`
-
# from within the constructor, thus allowing these options to be set at
-
# object creation.
-
#
-
# @param [Hash] opts the options defining dereference behavior.
-
# @option opts [String] :dup_on_deref (false) call `#dup` before returning the data
-
# @option opts [String] :freeze_on_deref (false) call `#freeze` before returning the data
-
# @option opts [String] :copy_on_deref (nil) call the given `Proc` passing
-
# the internal value and returning the value returned from the proc
-
1
def set_deref_options(opts = {})
-
10
synchronize{ ns_set_deref_options(opts) }
-
end
-
-
# @!macro dereferenceable_set_deref_options
-
# @!visibility private
-
1
def ns_set_deref_options(opts)
-
5
@dup_on_deref = opts[:dup_on_deref] || opts[:dup]
-
5
@freeze_on_deref = opts[:freeze_on_deref] || opts[:freeze]
-
5
@copy_on_deref = opts[:copy_on_deref] || opts[:copy]
-
5
@do_nothing_on_deref = !(@dup_on_deref || @freeze_on_deref || @copy_on_deref)
-
nil
-
end
-
-
# @!visibility private
-
1
def apply_deref_options(value)
-
return nil if value.nil?
-
return value if @do_nothing_on_deref
-
value = @copy_on_deref.call(value) if @copy_on_deref
-
value = value.dup if @dup_on_deref
-
value = value.freeze if @freeze_on_deref
-
value
-
end
-
end
-
end
-
end
-
1
require 'logger'
-
-
1
module Concurrent
-
1
module Concern
-
-
# Include where logging is needed
-
#
-
# @!visibility private
-
1
module Logging
-
1
include Logger::Severity
-
-
# Logs through {Concurrent.global_logger}, it can be overridden by setting @logger
-
# @param [Integer] level one of Logger::Severity constants
-
# @param [String] progname e.g. a path of an Actor
-
# @param [String, nil] message when nil block is used to generate the message
-
# @yieldreturn [String] a message
-
1
def log(level, progname, message = nil, &block)
-
#NOTE: Cannot require 'concurrent/configuration' above due to circular references.
-
# Assume that the gem has been initialized if we've gotten this far.
-
logger = if defined?(@logger) && @logger
-
@logger
-
else
-
Concurrent.global_logger
-
end
-
logger.call level, progname, message, &block
-
rescue => error
-
$stderr.puts "`Concurrent.configuration.logger` failed to log #{[level, progname, message, block]}\n" +
-
"#{error.message} (#{error.class})\n#{error.backtrace.join "\n"}"
-
end
-
end
-
end
-
end
-
1
require 'concurrent/collection/copy_on_notify_observer_set'
-
1
require 'concurrent/collection/copy_on_write_observer_set'
-
-
1
module Concurrent
-
1
module Concern
-
-
# The [observer pattern](http://en.wikipedia.org/wiki/Observer_pattern) is one
-
# of the most useful design patterns.
-
#
-
# The workflow is very simple:
-
# - an `observer` can register itself to a `subject` via a callback
-
# - many `observers` can be registered to the same `subject`
-
# - the `subject` notifies all registered observers when its status changes
-
# - an `observer` can deregister itself when is no more interested to receive
-
# event notifications
-
#
-
# In a single threaded environment the whole pattern is very easy: the
-
# `subject` can use a simple data structure to manage all its subscribed
-
# `observer`s and every `observer` can react directly to every event without
-
# caring about synchronization.
-
#
-
# In a multi threaded environment things are more complex. The `subject` must
-
# synchronize the access to its data structure and to do so currently we're
-
# using two specialized ObserverSet: {Concurrent::Concern::CopyOnWriteObserverSet}
-
# and {Concurrent::Concern::CopyOnNotifyObserverSet}.
-
#
-
# When implementing and `observer` there's a very important rule to remember:
-
# **there are no guarantees about the thread that will execute the callback**
-
#
-
# Let's take this example
-
# ```
-
# class Observer
-
# def initialize
-
# @count = 0
-
# end
-
#
-
# def update
-
# @count += 1
-
# end
-
# end
-
#
-
# obs = Observer.new
-
# [obj1, obj2, obj3, obj4].each { |o| o.add_observer(obs) }
-
# # execute [obj1, obj2, obj3, obj4]
-
# ```
-
#
-
# `obs` is wrong because the variable `@count` can be accessed by different
-
# threads at the same time, so it should be synchronized (using either a Mutex
-
# or an AtomicFixum)
-
1
module Observable
-
-
# @!macro observable_add_observer
-
#
-
# Adds an observer to this set. If a block is passed, the observer will be
-
# created by this method and no other params should be passed.
-
#
-
# @param [Object] observer the observer to add
-
# @param [Symbol] func the function to call on the observer during notification.
-
# Default is :update
-
# @return [Object] the added observer
-
1
def add_observer(observer = nil, func = :update, &block)
-
observers.add_observer(observer, func, &block)
-
end
-
-
# As `#add_observer` but can be used for chaining.
-
#
-
# @param [Object] observer the observer to add
-
# @param [Symbol] func the function to call on the observer during notification.
-
# @return [Observable] self
-
1
def with_observer(observer = nil, func = :update, &block)
-
add_observer(observer, func, &block)
-
self
-
end
-
-
# @!macro observable_delete_observer
-
#
-
# Remove `observer` as an observer on this object so that it will no
-
# longer receive notifications.
-
#
-
# @param [Object] observer the observer to remove
-
# @return [Object] the deleted observer
-
1
def delete_observer(observer)
-
observers.delete_observer(observer)
-
end
-
-
# @!macro observable_delete_observers
-
#
-
# Remove all observers associated with this object.
-
#
-
# @return [Observable] self
-
1
def delete_observers
-
observers.delete_observers
-
self
-
end
-
-
# @!macro observable_count_observers
-
#
-
# Return the number of observers associated with this object.
-
#
-
# @return [Integer] the observers count
-
1
def count_observers
-
observers.count_observers
-
end
-
-
1
protected
-
-
1
attr_accessor :observers
-
end
-
end
-
end
-
1
require 'thread'
-
1
require 'concurrent/delay'
-
1
require 'concurrent/errors'
-
1
require 'concurrent/atomic/atomic_reference'
-
1
require 'concurrent/concern/logging'
-
1
require 'concurrent/concern/deprecation'
-
1
require 'concurrent/executor/immediate_executor'
-
1
require 'concurrent/executor/cached_thread_pool'
-
1
require 'concurrent/utility/processor_counter'
-
-
1
module Concurrent
-
1
extend Concern::Logging
-
1
extend Concern::Deprecation
-
-
1
autoload :Options, 'concurrent/options'
-
1
autoload :TimerSet, 'concurrent/executor/timer_set'
-
1
autoload :ThreadPoolExecutor, 'concurrent/executor/thread_pool_executor'
-
-
# @return [Logger] Logger with provided level and output.
-
1
def self.create_simple_logger(level = Logger::FATAL, output = $stderr)
-
# TODO (pitr-ch 24-Dec-2016): figure out why it had to be replaced, stdlogger was deadlocking
-
1
lambda do |severity, progname, message = nil, &block|
-
return false if severity < level
-
-
message = block ? block.call : message
-
formatted_message = case message
-
when String
-
message
-
when Exception
-
format "%s (%s)\n%s",
-
message.message, message.class, (message.backtrace || []).join("\n")
-
else
-
message.inspect
-
end
-
-
output.print format "[%s] %5s -- %s: %s\n",
-
Time.now.strftime('%Y-%m-%d %H:%M:%S.%L'),
-
Logger::SEV_LABEL[severity],
-
progname,
-
formatted_message
-
true
-
end
-
end
-
-
# Use logger created by #create_simple_logger to log concurrent-ruby messages.
-
1
def self.use_simple_logger(level = Logger::FATAL, output = $stderr)
-
Concurrent.global_logger = create_simple_logger level, output
-
end
-
-
# @return [Logger] Logger with provided level and output.
-
# @deprecated
-
1
def self.create_stdlib_logger(level = Logger::FATAL, output = $stderr)
-
logger = Logger.new(output)
-
logger.level = level
-
logger.formatter = lambda do |severity, datetime, progname, msg|
-
formatted_message = case msg
-
when String
-
msg
-
when Exception
-
format "%s (%s)\n%s",
-
msg.message, msg.class, (msg.backtrace || []).join("\n")
-
else
-
msg.inspect
-
end
-
format "[%s] %5s -- %s: %s\n",
-
datetime.strftime('%Y-%m-%d %H:%M:%S.%L'),
-
severity,
-
progname,
-
formatted_message
-
end
-
-
lambda do |loglevel, progname, message = nil, &block|
-
logger.add loglevel, message, progname, &block
-
end
-
end
-
-
# Use logger created by #create_stdlib_logger to log concurrent-ruby messages.
-
# @deprecated
-
1
def self.use_stdlib_logger(level = Logger::FATAL, output = $stderr)
-
Concurrent.global_logger = create_stdlib_logger level, output
-
end
-
-
# TODO (pitr-ch 27-Dec-2016): remove deadlocking stdlib_logger methods
-
-
# Suppresses all output when used for logging.
-
1
NULL_LOGGER = lambda { |level, progname, message = nil, &block| }
-
-
# @!visibility private
-
1
GLOBAL_LOGGER = AtomicReference.new(create_simple_logger(Logger::WARN))
-
1
private_constant :GLOBAL_LOGGER
-
-
1
def self.global_logger
-
GLOBAL_LOGGER.value
-
end
-
-
1
def self.global_logger=(value)
-
GLOBAL_LOGGER.value = value
-
end
-
-
# @!visibility private
-
1
GLOBAL_FAST_EXECUTOR = Delay.new { Concurrent.new_fast_executor }
-
1
private_constant :GLOBAL_FAST_EXECUTOR
-
-
# @!visibility private
-
1
GLOBAL_IO_EXECUTOR = Delay.new { Concurrent.new_io_executor }
-
1
private_constant :GLOBAL_IO_EXECUTOR
-
-
# @!visibility private
-
1
GLOBAL_TIMER_SET = Delay.new { TimerSet.new }
-
1
private_constant :GLOBAL_TIMER_SET
-
-
# @!visibility private
-
1
GLOBAL_IMMEDIATE_EXECUTOR = ImmediateExecutor.new
-
1
private_constant :GLOBAL_IMMEDIATE_EXECUTOR
-
-
# Disables AtExit handlers including pool auto-termination handlers.
-
# When disabled it will be the application programmer's responsibility
-
# to ensure that the handlers are shutdown properly prior to application
-
# exit by calling `AtExit.run` method.
-
#
-
# @note this option should be needed only because of `at_exit` ordering
-
# issues which may arise when running some of the testing frameworks.
-
# E.g. Minitest's test-suite runs itself in `at_exit` callback which
-
# executes after the pools are already terminated. Then auto termination
-
# needs to be disabled and called manually after test-suite ends.
-
# @note This method should *never* be called
-
# from within a gem. It should *only* be used from within the main
-
# application and even then it should be used only when necessary.
-
# @deprecated Has no effect since it is no longer needed, see https://github.com/ruby-concurrency/concurrent-ruby/pull/841.
-
#
-
1
def self.disable_at_exit_handlers!
-
deprecated "Method #disable_at_exit_handlers! has no effect since it is no longer needed, see https://github.com/ruby-concurrency/concurrent-ruby/pull/841."
-
end
-
-
# Global thread pool optimized for short, fast *operations*.
-
#
-
# @return [ThreadPoolExecutor] the thread pool
-
1
def self.global_fast_executor
-
GLOBAL_FAST_EXECUTOR.value
-
end
-
-
# Global thread pool optimized for long, blocking (IO) *tasks*.
-
#
-
# @return [ThreadPoolExecutor] the thread pool
-
1
def self.global_io_executor
-
GLOBAL_IO_EXECUTOR.value
-
end
-
-
1
def self.global_immediate_executor
-
GLOBAL_IMMEDIATE_EXECUTOR
-
end
-
-
# Global thread pool user for global *timers*.
-
#
-
# @return [Concurrent::TimerSet] the thread pool
-
1
def self.global_timer_set
-
GLOBAL_TIMER_SET.value
-
end
-
-
# General access point to global executors.
-
# @param [Symbol, Executor] executor_identifier symbols:
-
# - :fast - {Concurrent.global_fast_executor}
-
# - :io - {Concurrent.global_io_executor}
-
# - :immediate - {Concurrent.global_immediate_executor}
-
# @return [Executor]
-
1
def self.executor(executor_identifier)
-
Options.executor(executor_identifier)
-
end
-
-
1
def self.new_fast_executor(opts = {})
-
FixedThreadPool.new(
-
[2, Concurrent.processor_count].max,
-
auto_terminate: opts.fetch(:auto_terminate, true),
-
idletime: 60, # 1 minute
-
max_queue: 0, # unlimited
-
fallback_policy: :abort, # shouldn't matter -- 0 max queue
-
name: "fast"
-
)
-
end
-
-
1
def self.new_io_executor(opts = {})
-
CachedThreadPool.new(
-
auto_terminate: opts.fetch(:auto_terminate, true),
-
fallback_policy: :abort, # shouldn't matter -- 0 max queue
-
name: "io"
-
)
-
end
-
end
-
1
module Concurrent
-
-
# Various classes within allows for +nil+ values to be stored,
-
# so a special +NULL+ token is required to indicate the "nil-ness".
-
# @!visibility private
-
1
NULL = ::Object.new
-
-
end
-
1
require 'concurrent/future'
-
1
require 'concurrent/atomic/atomic_fixnum'
-
-
1
module Concurrent
-
-
# @!visibility private
-
1
class DependencyCounter # :nodoc:
-
-
1
def initialize(count, &block)
-
@counter = AtomicFixnum.new(count)
-
@block = block
-
end
-
-
1
def update(time, value, reason)
-
if @counter.decrement == 0
-
@block.call
-
end
-
end
-
end
-
-
# Dataflow allows you to create a task that will be scheduled when all of its data dependencies are available.
-
# {include:file:docs-source/dataflow.md}
-
#
-
# @param [Future] inputs zero or more `Future` operations that this dataflow depends upon
-
#
-
# @yield The operation to perform once all the dependencies are met
-
# @yieldparam [Future] inputs each of the `Future` inputs to the dataflow
-
# @yieldreturn [Object] the result of the block operation
-
#
-
# @return [Object] the result of all the operations
-
#
-
# @raise [ArgumentError] if no block is given
-
# @raise [ArgumentError] if any of the inputs are not `IVar`s
-
1
def dataflow(*inputs, &block)
-
dataflow_with(Concurrent.global_io_executor, *inputs, &block)
-
end
-
1
module_function :dataflow
-
-
1
def dataflow_with(executor, *inputs, &block)
-
call_dataflow(:value, executor, *inputs, &block)
-
end
-
1
module_function :dataflow_with
-
-
1
def dataflow!(*inputs, &block)
-
dataflow_with!(Concurrent.global_io_executor, *inputs, &block)
-
end
-
1
module_function :dataflow!
-
-
1
def dataflow_with!(executor, *inputs, &block)
-
call_dataflow(:value!, executor, *inputs, &block)
-
end
-
1
module_function :dataflow_with!
-
-
1
private
-
-
1
def call_dataflow(method, executor, *inputs, &block)
-
raise ArgumentError.new('an executor must be provided') if executor.nil?
-
raise ArgumentError.new('no block given') unless block_given?
-
unless inputs.all? { |input| input.is_a? IVar }
-
raise ArgumentError.new("Not all dependencies are IVars.\nDependencies: #{ inputs.inspect }")
-
end
-
-
result = Future.new(executor: executor) do
-
values = inputs.map { |input| input.send(method) }
-
block.call(*values)
-
end
-
-
if inputs.empty?
-
result.execute
-
else
-
counter = DependencyCounter.new(inputs.size) { result.execute }
-
-
inputs.each do |input|
-
input.add_observer counter
-
end
-
end
-
-
result
-
end
-
1
module_function :call_dataflow
-
end
-
1
require 'thread'
-
1
require 'concurrent/concern/obligation'
-
1
require 'concurrent/executor/immediate_executor'
-
1
require 'concurrent/synchronization'
-
-
1
module Concurrent
-
-
# This file has circular require issues. It must be autoloaded here.
-
1
autoload :Options, 'concurrent/options'
-
-
# Lazy evaluation of a block yielding an immutable result. Useful for
-
# expensive operations that may never be needed. It may be non-blocking,
-
# supports the `Concern::Obligation` interface, and accepts the injection of
-
# custom executor upon which to execute the block. Processing of
-
# block will be deferred until the first time `#value` is called.
-
# At that time the caller can choose to return immediately and let
-
# the block execute asynchronously, block indefinitely, or block
-
# with a timeout.
-
#
-
# When a `Delay` is created its state is set to `pending`. The value and
-
# reason are both `nil`. The first time the `#value` method is called the
-
# enclosed opration will be run and the calling thread will block. Other
-
# threads attempting to call `#value` will block as well. Once the operation
-
# is complete the *value* will be set to the result of the operation or the
-
# *reason* will be set to the raised exception, as appropriate. All threads
-
# blocked on `#value` will return. Subsequent calls to `#value` will immediately
-
# return the cached value. The operation will only be run once. This means that
-
# any side effects created by the operation will only happen once as well.
-
#
-
# `Delay` includes the `Concurrent::Concern::Dereferenceable` mixin to support thread
-
# safety of the reference returned by `#value`.
-
#
-
# @!macro copy_options
-
#
-
# @!macro delay_note_regarding_blocking
-
# @note The default behavior of `Delay` is to block indefinitely when
-
# calling either `value` or `wait`, executing the delayed operation on
-
# the current thread. This makes the `timeout` value completely
-
# irrelevant. To enable non-blocking behavior, use the `executor`
-
# constructor option. This will cause the delayed operation to be
-
# execute on the given executor, allowing the call to timeout.
-
#
-
# @see Concurrent::Concern::Dereferenceable
-
1
class Delay < Synchronization::LockableObject
-
1
include Concern::Obligation
-
-
# NOTE: Because the global thread pools are lazy-loaded with these objects
-
# there is a performance hit every time we post a new task to one of these
-
# thread pools. Subsequently it is critical that `Delay` perform as fast
-
# as possible post-completion. This class has been highly optimized using
-
# the benchmark script `examples/lazy_and_delay.rb`. Do NOT attempt to
-
# DRY-up this class or perform other refactoring with running the
-
# benchmarks and ensuring that performance is not negatively impacted.
-
-
# Create a new `Delay` in the `:pending` state.
-
#
-
# @!macro executor_and_deref_options
-
#
-
# @yield the delayed operation to perform
-
#
-
# @raise [ArgumentError] if no block is given
-
1
def initialize(opts = {}, &block)
-
5
raise ArgumentError.new('no block given') unless block_given?
-
5
super(&nil)
-
10
synchronize { ns_initialize(opts, &block) }
-
end
-
-
# Return the value this object represents after applying the options
-
# specified by the `#set_deref_options` method. If the delayed operation
-
# raised an exception this method will return nil. The execption object
-
# can be accessed via the `#reason` method.
-
#
-
# @param [Numeric] timeout the maximum number of seconds to wait
-
# @return [Object] the current value of the object
-
#
-
# @!macro delay_note_regarding_blocking
-
1
def value(timeout = nil)
-
if @executor # TODO (pitr 12-Sep-2015): broken unsafe read?
-
super
-
else
-
# this function has been optimized for performance and
-
# should not be modified without running new benchmarks
-
synchronize do
-
execute = @evaluation_started = true unless @evaluation_started
-
if execute
-
begin
-
set_state(true, @task.call, nil)
-
rescue => ex
-
set_state(false, nil, ex)
-
end
-
elsif incomplete?
-
raise IllegalOperationError, 'Recursive call to #value during evaluation of the Delay'
-
end
-
end
-
if @do_nothing_on_deref
-
@value
-
else
-
apply_deref_options(@value)
-
end
-
end
-
end
-
-
# Return the value this object represents after applying the options
-
# specified by the `#set_deref_options` method. If the delayed operation
-
# raised an exception, this method will raise that exception (even when)
-
# the operation has already been executed).
-
#
-
# @param [Numeric] timeout the maximum number of seconds to wait
-
# @return [Object] the current value of the object
-
# @raise [Exception] when `#rejected?` raises `#reason`
-
#
-
# @!macro delay_note_regarding_blocking
-
1
def value!(timeout = nil)
-
if @executor
-
super
-
else
-
result = value
-
raise @reason if @reason
-
result
-
end
-
end
-
-
# Return the value this object represents after applying the options
-
# specified by the `#set_deref_options` method.
-
#
-
# @param [Integer] timeout (nil) the maximum number of seconds to wait for
-
# the value to be computed. When `nil` the caller will block indefinitely.
-
#
-
# @return [Object] self
-
#
-
# @!macro delay_note_regarding_blocking
-
1
def wait(timeout = nil)
-
if @executor
-
execute_task_once
-
super(timeout)
-
else
-
value
-
end
-
self
-
end
-
-
# Reconfigures the block returning the value if still `#incomplete?`
-
#
-
# @yield the delayed operation to perform
-
# @return [true, false] if success
-
1
def reconfigure(&block)
-
synchronize do
-
raise ArgumentError.new('no block given') unless block_given?
-
unless @evaluation_started
-
@task = block
-
true
-
else
-
false
-
end
-
end
-
end
-
-
1
protected
-
-
1
def ns_initialize(opts, &block)
-
5
init_obligation
-
5
set_deref_options(opts)
-
5
@executor = opts[:executor]
-
-
5
@task = block
-
5
@state = :pending
-
5
@evaluation_started = false
-
end
-
-
1
private
-
-
# @!visibility private
-
1
def execute_task_once # :nodoc:
-
# this function has been optimized for performance and
-
# should not be modified without running new benchmarks
-
execute = task = nil
-
synchronize do
-
execute = @evaluation_started = true unless @evaluation_started
-
task = @task
-
end
-
-
if execute
-
executor = Options.executor_from_options(executor: @executor)
-
executor.post do
-
begin
-
result = task.call
-
success = true
-
rescue => ex
-
reason = ex
-
end
-
synchronize do
-
set_state(success, result, reason)
-
event.set
-
end
-
end
-
end
-
end
-
end
-
end
-
1
module Concurrent
-
-
1
Error = Class.new(StandardError)
-
-
# Raised when errors occur during configuration.
-
1
ConfigurationError = Class.new(Error)
-
-
# Raised when an asynchronous operation is cancelled before execution.
-
1
CancelledOperationError = Class.new(Error)
-
-
# Raised when a lifecycle method (such as `stop`) is called in an improper
-
# sequence or when the object is in an inappropriate state.
-
1
LifecycleError = Class.new(Error)
-
-
# Raised when an attempt is made to violate an immutability guarantee.
-
1
ImmutabilityError = Class.new(Error)
-
-
# Raised when an operation is attempted which is not legal given the
-
# receiver's current state
-
1
IllegalOperationError = Class.new(Error)
-
-
# Raised when an object's methods are called when it has not been
-
# properly initialized.
-
1
InitializationError = Class.new(Error)
-
-
# Raised when an object with a start/stop lifecycle has been started an
-
# excessive number of times. Often used in conjunction with a restart
-
# policy or strategy.
-
1
MaxRestartFrequencyError = Class.new(Error)
-
-
# Raised when an attempt is made to modify an immutable object
-
# (such as an `IVar`) after its final state has been set.
-
1
class MultipleAssignmentError < Error
-
1
attr_reader :inspection_data
-
-
1
def initialize(message = nil, inspection_data = nil)
-
@inspection_data = inspection_data
-
super message
-
end
-
-
1
def inspect
-
format '%s %s>', super[0..-2], @inspection_data.inspect
-
end
-
end
-
-
# Raised by an `Executor` when it is unable to process a given task,
-
# possibly because of a reject policy or other internal error.
-
1
RejectedExecutionError = Class.new(Error)
-
-
# Raised when any finite resource, such as a lock counter, exceeds its
-
# maximum limit/threshold.
-
1
ResourceLimitError = Class.new(Error)
-
-
# Raised when an operation times out.
-
1
TimeoutError = Class.new(Error)
-
-
# Aggregates multiple exceptions.
-
1
class MultipleErrors < Error
-
1
attr_reader :errors
-
-
1
def initialize(errors, message = "#{errors.size} errors")
-
@errors = errors
-
super [*message,
-
*errors.map { |e| [format('%s (%s)', e.message, e.class), *e.backtrace] }.flatten(1)
-
].join("\n")
-
end
-
end
-
-
end
-
1
require 'concurrent/constants'
-
1
require 'concurrent/errors'
-
1
require 'concurrent/maybe'
-
1
require 'concurrent/atomic/atomic_reference'
-
1
require 'concurrent/atomic/count_down_latch'
-
1
require 'concurrent/utility/engine'
-
1
require 'concurrent/utility/monotonic_time'
-
-
1
module Concurrent
-
-
# @!macro exchanger
-
#
-
# A synchronization point at which threads can pair and swap elements within
-
# pairs. Each thread presents some object on entry to the exchange method,
-
# matches with a partner thread, and receives its partner's object on return.
-
#
-
# @!macro thread_safe_variable_comparison
-
#
-
# This implementation is very simple, using only a single slot for each
-
# exchanger (unlike more advanced implementations which use an "arena").
-
# This approach will work perfectly fine when there are only a few threads
-
# accessing a single `Exchanger`. Beyond a handful of threads the performance
-
# will degrade rapidly due to contention on the single slot, but the algorithm
-
# will remain correct.
-
#
-
# @see http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/Exchanger.html java.util.concurrent.Exchanger
-
# @example
-
#
-
# exchanger = Concurrent::Exchanger.new
-
#
-
# threads = [
-
# Thread.new { puts "first: " << exchanger.exchange('foo', 1) }, #=> "first: bar"
-
# Thread.new { puts "second: " << exchanger.exchange('bar', 1) } #=> "second: foo"
-
# ]
-
# threads.each {|t| t.join(2) }
-
-
# @!visibility private
-
1
class AbstractExchanger < Synchronization::Object
-
-
# @!visibility private
-
1
CANCEL = ::Object.new
-
1
private_constant :CANCEL
-
-
1
def initialize
-
super
-
end
-
-
# @!macro exchanger_method_do_exchange
-
#
-
# Waits for another thread to arrive at this exchange point (unless the
-
# current thread is interrupted), and then transfers the given object to
-
# it, receiving its object in return. The timeout value indicates the
-
# approximate number of seconds the method should block while waiting
-
# for the exchange. When the timeout value is `nil` the method will
-
# block indefinitely.
-
#
-
# @param [Object] value the value to exchange with another thread
-
# @param [Numeric, nil] timeout in seconds, `nil` blocks indefinitely
-
#
-
# @!macro exchanger_method_exchange
-
#
-
# In some edge cases when a `timeout` is given a return value of `nil` may be
-
# ambiguous. Specifically, if `nil` is a valid value in the exchange it will
-
# be impossible to tell whether `nil` is the actual return value or if it
-
# signifies timeout. When `nil` is a valid value in the exchange consider
-
# using {#exchange!} or {#try_exchange} instead.
-
#
-
# @return [Object] the value exchanged by the other thread or `nil` on timeout
-
1
def exchange(value, timeout = nil)
-
(value = do_exchange(value, timeout)) == CANCEL ? nil : value
-
end
-
-
# @!macro exchanger_method_do_exchange
-
# @!macro exchanger_method_exchange_bang
-
#
-
# On timeout a {Concurrent::TimeoutError} exception will be raised.
-
#
-
# @return [Object] the value exchanged by the other thread
-
# @raise [Concurrent::TimeoutError] on timeout
-
1
def exchange!(value, timeout = nil)
-
if (value = do_exchange(value, timeout)) == CANCEL
-
raise Concurrent::TimeoutError
-
else
-
value
-
end
-
end
-
-
# @!macro exchanger_method_do_exchange
-
# @!macro exchanger_method_try_exchange
-
#
-
# The return value will be a {Concurrent::Maybe} set to `Just` on success or
-
# `Nothing` on timeout.
-
#
-
# @return [Concurrent::Maybe] on success a `Just` maybe will be returned with
-
# the item exchanged by the other thread as `#value`; on timeout a
-
# `Nothing` maybe will be returned with {Concurrent::TimeoutError} as `#reason`
-
#
-
# @example
-
#
-
# exchanger = Concurrent::Exchanger.new
-
#
-
# result = exchanger.exchange(:foo, 0.5)
-
#
-
# if result.just?
-
# puts result.value #=> :bar
-
# else
-
# puts 'timeout'
-
# end
-
1
def try_exchange(value, timeout = nil)
-
if (value = do_exchange(value, timeout)) == CANCEL
-
Concurrent::Maybe.nothing(Concurrent::TimeoutError)
-
else
-
Concurrent::Maybe.just(value)
-
end
-
end
-
-
1
private
-
-
# @!macro exchanger_method_do_exchange
-
#
-
# @return [Object, CANCEL] the value exchanged by the other thread; {CANCEL} on timeout
-
1
def do_exchange(value, timeout)
-
raise NotImplementedError
-
end
-
end
-
-
# @!macro internal_implementation_note
-
# @!visibility private
-
1
class RubyExchanger < AbstractExchanger
-
# A simplified version of java.util.concurrent.Exchanger written by
-
# Doug Lea, Bill Scherer, and Michael Scott with assistance from members
-
# of JCP JSR-166 Expert Group and released to the public domain. It does
-
# not include the arena or the multi-processor spin loops.
-
# http://grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/6-b14/java/util/concurrent/Exchanger.java
-
-
1
safe_initialization!
-
-
1
class Node < Concurrent::Synchronization::Object
-
1
attr_atomic :value
-
1
safe_initialization!
-
-
1
def initialize(item)
-
super()
-
@Item = item
-
@Latch = Concurrent::CountDownLatch.new
-
self.value = nil
-
end
-
-
1
def latch
-
@Latch
-
end
-
-
1
def item
-
@Item
-
end
-
end
-
1
private_constant :Node
-
-
1
def initialize
-
super
-
end
-
-
1
private
-
-
1
attr_atomic(:slot)
-
-
# @!macro exchanger_method_do_exchange
-
#
-
# @return [Object, CANCEL] the value exchanged by the other thread; {CANCEL} on timeout
-
1
def do_exchange(value, timeout)
-
-
# ALGORITHM
-
#
-
# From the original Java version:
-
#
-
# > The basic idea is to maintain a "slot", which is a reference to
-
# > a Node containing both an Item to offer and a "hole" waiting to
-
# > get filled in. If an incoming "occupying" thread sees that the
-
# > slot is null, it CAS'es (compareAndSets) a Node there and waits
-
# > for another to invoke exchange. That second "fulfilling" thread
-
# > sees that the slot is non-null, and so CASes it back to null,
-
# > also exchanging items by CASing the hole, plus waking up the
-
# > occupying thread if it is blocked. In each case CAS'es may
-
# > fail because a slot at first appears non-null but is null upon
-
# > CAS, or vice-versa. So threads may need to retry these
-
# > actions.
-
#
-
# This version:
-
#
-
# An exchange occurs between an "occupier" thread and a "fulfiller" thread.
-
# The "slot" is used to setup this interaction. The first thread in the
-
# exchange puts itself into the slot (occupies) and waits for a fulfiller.
-
# The second thread removes the occupier from the slot and attempts to
-
# perform the exchange. Removing the occupier also frees the slot for
-
# another occupier/fulfiller pair.
-
#
-
# Because the occupier and the fulfiller are operating independently and
-
# because there may be contention with other threads, any failed operation
-
# indicates contention. Both the occupier and the fulfiller operate within
-
# spin loops. Any failed actions along the happy path will cause the thread
-
# to repeat the loop and try again.
-
#
-
# When a timeout value is given the thread must be cognizant of time spent
-
# in the spin loop. The remaining time is checked every loop. When the time
-
# runs out the thread will exit.
-
#
-
# A "node" is the data structure used to perform the exchange. Only the
-
# occupier's node is necessary. It's the node used for the exchange.
-
# Each node has an "item," a "hole" (self), and a "latch." The item is the
-
# node's initial value. It never changes. It's what the fulfiller returns on
-
# success. The occupier's hole is where the fulfiller put its item. It's the
-
# item that the occupier returns on success. The latch is used for synchronization.
-
# Because a thread may act as either an occupier or fulfiller (or possibly
-
# both in periods of high contention) every thread creates a node when
-
# the exchange method is first called.
-
#
-
# The following steps occur within the spin loop. If any actions fail
-
# the thread will loop and try again, so long as there is time remaining.
-
# If time runs out the thread will return CANCEL.
-
#
-
# Check the slot for an occupier:
-
#
-
# * If the slot is empty try to occupy
-
# * If the slot is full try to fulfill
-
#
-
# Attempt to occupy:
-
#
-
# * Attempt to CAS myself into the slot
-
# * Go to sleep and wait to be woken by a fulfiller
-
# * If the sleep is successful then the fulfiller completed its happy path
-
# - Return the value from my hole (the value given by the fulfiller)
-
# * When the sleep fails (time ran out) attempt to cancel the operation
-
# - Attempt to CAS myself out of the hole
-
# - If successful there is no contention
-
# - Return CANCEL
-
# - On failure, I am competing with a fulfiller
-
# - Attempt to CAS my hole to CANCEL
-
# - On success
-
# - Let the fulfiller deal with my cancel
-
# - Return CANCEL
-
# - On failure the fulfiller has completed its happy path
-
# - Return th value from my hole (the fulfiller's value)
-
#
-
# Attempt to fulfill:
-
#
-
# * Attempt to CAS the occupier out of the slot
-
# - On failure loop again
-
# * Attempt to CAS my item into the occupier's hole
-
# - On failure the occupier is trying to cancel
-
# - Loop again
-
# - On success we are on the happy path
-
# - Wake the sleeping occupier
-
# - Return the occupier's item
-
-
value = NULL if value.nil? # The sentinel allows nil to be a valid value
-
me = Node.new(value) # create my node in case I need to occupy
-
end_at = Concurrent.monotonic_time + timeout.to_f # The time to give up
-
-
result = loop do
-
other = slot
-
if other && compare_and_set_slot(other, nil)
-
# try to fulfill
-
if other.compare_and_set_value(nil, value)
-
# happy path
-
other.latch.count_down
-
break other.item
-
end
-
elsif other.nil? && compare_and_set_slot(nil, me)
-
# try to occupy
-
timeout = end_at - Concurrent.monotonic_time if timeout
-
if me.latch.wait(timeout)
-
# happy path
-
break me.value
-
else
-
# attempt to remove myself from the slot
-
if compare_and_set_slot(me, nil)
-
break CANCEL
-
elsif !me.compare_and_set_value(nil, CANCEL)
-
# I've failed to block the fulfiller
-
break me.value
-
end
-
end
-
end
-
break CANCEL if timeout && Concurrent.monotonic_time >= end_at
-
end
-
-
result == NULL ? nil : result
-
end
-
end
-
-
1
if Concurrent.on_jruby?
-
-
# @!macro internal_implementation_note
-
# @!visibility private
-
class JavaExchanger < AbstractExchanger
-
-
def initialize
-
@exchanger = java.util.concurrent.Exchanger.new
-
end
-
-
private
-
-
# @!macro exchanger_method_do_exchange
-
#
-
# @return [Object, CANCEL] the value exchanged by the other thread; {CANCEL} on timeout
-
def do_exchange(value, timeout)
-
result = nil
-
if timeout.nil?
-
Synchronization::JRuby.sleep_interruptibly do
-
result = @exchanger.exchange(value)
-
end
-
else
-
Synchronization::JRuby.sleep_interruptibly do
-
result = @exchanger.exchange(value, 1000 * timeout, java.util.concurrent.TimeUnit::MILLISECONDS)
-
end
-
end
-
result
-
rescue java.util.concurrent.TimeoutException
-
CANCEL
-
end
-
end
-
end
-
-
# @!visibility private
-
# @!macro internal_implementation_note
-
ExchangerImplementation = case
-
1
when Concurrent.on_jruby?
-
JavaExchanger
-
else
-
1
RubyExchanger
-
end
-
1
private_constant :ExchangerImplementation
-
-
# @!macro exchanger
-
1
class Exchanger < ExchangerImplementation
-
-
# @!method initialize
-
# Creates exchanger instance
-
-
# @!method exchange(value, timeout = nil)
-
# @!macro exchanger_method_do_exchange
-
# @!macro exchanger_method_exchange
-
-
# @!method exchange!(value, timeout = nil)
-
# @!macro exchanger_method_do_exchange
-
# @!macro exchanger_method_exchange_bang
-
-
# @!method try_exchange(value, timeout = nil)
-
# @!macro exchanger_method_do_exchange
-
# @!macro exchanger_method_try_exchange
-
end
-
end
-
1
require 'concurrent/utility/engine'
-
1
require 'concurrent/executor/thread_pool_executor'
-
-
1
module Concurrent
-
-
# A thread pool that dynamically grows and shrinks to fit the current workload.
-
# New threads are created as needed, existing threads are reused, and threads
-
# that remain idle for too long are killed and removed from the pool. These
-
# pools are particularly suited to applications that perform a high volume of
-
# short-lived tasks.
-
#
-
# On creation a `CachedThreadPool` has zero running threads. New threads are
-
# created on the pool as new operations are `#post`. The size of the pool
-
# will grow until `#max_length` threads are in the pool or until the number
-
# of threads exceeds the number of running and pending operations. When a new
-
# operation is post to the pool the first available idle thread will be tasked
-
# with the new operation.
-
#
-
# Should a thread crash for any reason the thread will immediately be removed
-
# from the pool. Similarly, threads which remain idle for an extended period
-
# of time will be killed and reclaimed. Thus these thread pools are very
-
# efficient at reclaiming unused resources.
-
#
-
# The API and behavior of this class are based on Java's `CachedThreadPool`
-
#
-
# @!macro thread_pool_options
-
1
class CachedThreadPool < ThreadPoolExecutor
-
-
# @!macro cached_thread_pool_method_initialize
-
#
-
# Create a new thread pool.
-
#
-
# @param [Hash] opts the options defining pool behavior.
-
# @option opts [Symbol] :fallback_policy (`:abort`) the fallback policy
-
#
-
# @raise [ArgumentError] if `fallback_policy` is not a known policy
-
#
-
# @see http://docs.oracle.com/javase/8/docs/api/java/util/concurrent/Executors.html#newCachedThreadPool--
-
1
def initialize(opts = {})
-
defaults = { idletime: DEFAULT_THREAD_IDLETIMEOUT }
-
overrides = { min_threads: 0,
-
max_threads: DEFAULT_MAX_POOL_SIZE,
-
max_queue: DEFAULT_MAX_QUEUE_SIZE }
-
super(defaults.merge(opts).merge(overrides))
-
end
-
-
1
private
-
-
# @!macro cached_thread_pool_method_initialize
-
# @!visibility private
-
1
def ns_initialize(opts)
-
super(opts)
-
if Concurrent.on_jruby?
-
@max_queue = 0
-
@executor = java.util.concurrent.Executors.newCachedThreadPool(
-
DaemonThreadFactory.new(ns_auto_terminate?))
-
@executor.setRejectedExecutionHandler(FALLBACK_POLICY_CLASSES[@fallback_policy].new)
-
@executor.setKeepAliveTime(opts.fetch(:idletime, DEFAULT_THREAD_IDLETIMEOUT), java.util.concurrent.TimeUnit::SECONDS)
-
end
-
end
-
end
-
end
-
1
require 'concurrent/utility/engine'
-
1
require 'concurrent/executor/thread_pool_executor'
-
-
1
module Concurrent
-
-
# @!macro thread_pool_executor_constant_default_max_pool_size
-
# Default maximum number of threads that will be created in the pool.
-
-
# @!macro thread_pool_executor_constant_default_min_pool_size
-
# Default minimum number of threads that will be retained in the pool.
-
-
# @!macro thread_pool_executor_constant_default_max_queue_size
-
# Default maximum number of tasks that may be added to the task queue.
-
-
# @!macro thread_pool_executor_constant_default_thread_timeout
-
# Default maximum number of seconds a thread in the pool may remain idle
-
# before being reclaimed.
-
-
# @!macro thread_pool_executor_attr_reader_max_length
-
# The maximum number of threads that may be created in the pool.
-
# @return [Integer] The maximum number of threads that may be created in the pool.
-
-
# @!macro thread_pool_executor_attr_reader_min_length
-
# The minimum number of threads that may be retained in the pool.
-
# @return [Integer] The minimum number of threads that may be retained in the pool.
-
-
# @!macro thread_pool_executor_attr_reader_largest_length
-
# The largest number of threads that have been created in the pool since construction.
-
# @return [Integer] The largest number of threads that have been created in the pool since construction.
-
-
# @!macro thread_pool_executor_attr_reader_scheduled_task_count
-
# The number of tasks that have been scheduled for execution on the pool since construction.
-
# @return [Integer] The number of tasks that have been scheduled for execution on the pool since construction.
-
-
# @!macro thread_pool_executor_attr_reader_completed_task_count
-
# The number of tasks that have been completed by the pool since construction.
-
# @return [Integer] The number of tasks that have been completed by the pool since construction.
-
-
# @!macro thread_pool_executor_attr_reader_idletime
-
# The number of seconds that a thread may be idle before being reclaimed.
-
# @return [Integer] The number of seconds that a thread may be idle before being reclaimed.
-
-
# @!macro thread_pool_executor_attr_reader_max_queue
-
# The maximum number of tasks that may be waiting in the work queue at any one time.
-
# When the queue size reaches `max_queue` subsequent tasks will be rejected in
-
# accordance with the configured `fallback_policy`.
-
#
-
# @return [Integer] The maximum number of tasks that may be waiting in the work queue at any one time.
-
# When the queue size reaches `max_queue` subsequent tasks will be rejected in
-
# accordance with the configured `fallback_policy`.
-
-
# @!macro thread_pool_executor_attr_reader_length
-
# The number of threads currently in the pool.
-
# @return [Integer] The number of threads currently in the pool.
-
-
# @!macro thread_pool_executor_attr_reader_queue_length
-
# The number of tasks in the queue awaiting execution.
-
# @return [Integer] The number of tasks in the queue awaiting execution.
-
-
# @!macro thread_pool_executor_attr_reader_remaining_capacity
-
# Number of tasks that may be enqueued before reaching `max_queue` and rejecting
-
# new tasks. A value of -1 indicates that the queue may grow without bound.
-
#
-
# @return [Integer] Number of tasks that may be enqueued before reaching `max_queue` and rejecting
-
# new tasks. A value of -1 indicates that the queue may grow without bound.
-
-
-
-
-
-
# @!macro thread_pool_executor_public_api
-
#
-
# @!macro abstract_executor_service_public_api
-
#
-
# @!attribute [r] max_length
-
# @!macro thread_pool_executor_attr_reader_max_length
-
#
-
# @!attribute [r] min_length
-
# @!macro thread_pool_executor_attr_reader_min_length
-
#
-
# @!attribute [r] largest_length
-
# @!macro thread_pool_executor_attr_reader_largest_length
-
#
-
# @!attribute [r] scheduled_task_count
-
# @!macro thread_pool_executor_attr_reader_scheduled_task_count
-
#
-
# @!attribute [r] completed_task_count
-
# @!macro thread_pool_executor_attr_reader_completed_task_count
-
#
-
# @!attribute [r] idletime
-
# @!macro thread_pool_executor_attr_reader_idletime
-
#
-
# @!attribute [r] max_queue
-
# @!macro thread_pool_executor_attr_reader_max_queue
-
#
-
# @!attribute [r] length
-
# @!macro thread_pool_executor_attr_reader_length
-
#
-
# @!attribute [r] queue_length
-
# @!macro thread_pool_executor_attr_reader_queue_length
-
#
-
# @!attribute [r] remaining_capacity
-
# @!macro thread_pool_executor_attr_reader_remaining_capacity
-
#
-
# @!method can_overflow?
-
# @!macro executor_service_method_can_overflow_question
-
-
-
-
-
# @!macro thread_pool_options
-
#
-
# **Thread Pool Options**
-
#
-
# Thread pools support several configuration options:
-
#
-
# * `idletime`: The number of seconds that a thread may be idle before being reclaimed.
-
# * `name`: The name of the executor (optional). Printed in the executor's `#to_s` output and
-
# a `<name>-worker-<id>` name is given to its threads if supported by used Ruby
-
# implementation. `<id>` is uniq for each thread.
-
# * `max_queue`: The maximum number of tasks that may be waiting in the work queue at
-
# any one time. When the queue size reaches `max_queue` and no new threads can be created,
-
# subsequent tasks will be rejected in accordance with the configured `fallback_policy`.
-
# * `auto_terminate`: When true (default), the threads started will be marked as daemon.
-
# * `fallback_policy`: The policy defining how rejected tasks are handled.
-
#
-
# Three fallback policies are supported:
-
#
-
# * `:abort`: Raise a `RejectedExecutionError` exception and discard the task.
-
# * `:discard`: Discard the task and return false.
-
# * `:caller_runs`: Execute the task on the calling thread.
-
#
-
# **Shutting Down Thread Pools**
-
#
-
# Killing a thread pool while tasks are still being processed, either by calling
-
# the `#kill` method or at application exit, will have unpredictable results. There
-
# is no way for the thread pool to know what resources are being used by the
-
# in-progress tasks. When those tasks are killed the impact on those resources
-
# cannot be predicted. The *best* practice is to explicitly shutdown all thread
-
# pools using the provided methods:
-
#
-
# * Call `#shutdown` to initiate an orderly termination of all in-progress tasks
-
# * Call `#wait_for_termination` with an appropriate timeout interval an allow
-
# the orderly shutdown to complete
-
# * Call `#kill` *only when* the thread pool fails to shutdown in the allotted time
-
#
-
# On some runtime platforms (most notably the JVM) the application will not
-
# exit until all thread pools have been shutdown. To prevent applications from
-
# "hanging" on exit, all threads can be marked as daemon according to the
-
# `:auto_terminate` option.
-
#
-
# ```ruby
-
# pool1 = Concurrent::FixedThreadPool.new(5) # threads will be marked as daemon
-
# pool2 = Concurrent::FixedThreadPool.new(5, auto_terminate: false) # mark threads as non-daemon
-
# ```
-
#
-
# @note Failure to properly shutdown a thread pool can lead to unpredictable results.
-
# Please read *Shutting Down Thread Pools* for more information.
-
#
-
# @see http://docs.oracle.com/javase/tutorial/essential/concurrency/pools.html Java Tutorials: Thread Pools
-
# @see http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/Executors.html Java Executors class
-
# @see http://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ExecutorService.html Java ExecutorService interface
-
# @see https://docs.oracle.com/javase/8/docs/api/java/lang/Thread.html#setDaemon-boolean-
-
-
-
-
-
-
# @!macro fixed_thread_pool
-
#
-
# A thread pool that reuses a fixed number of threads operating off an unbounded queue.
-
# At any point, at most `num_threads` will be active processing tasks. When all threads are busy new
-
# tasks `#post` to the thread pool are enqueued until a thread becomes available.
-
# Should a thread crash for any reason the thread will immediately be removed
-
# from the pool and replaced.
-
#
-
# The API and behavior of this class are based on Java's `FixedThreadPool`
-
#
-
# @!macro thread_pool_options
-
1
class FixedThreadPool < ThreadPoolExecutor
-
-
# @!macro fixed_thread_pool_method_initialize
-
#
-
# Create a new thread pool.
-
#
-
# @param [Integer] num_threads the number of threads to allocate
-
# @param [Hash] opts the options defining pool behavior.
-
# @option opts [Symbol] :fallback_policy (`:abort`) the fallback policy
-
#
-
# @raise [ArgumentError] if `num_threads` is less than or equal to zero
-
# @raise [ArgumentError] if `fallback_policy` is not a known policy
-
#
-
# @see http://docs.oracle.com/javase/8/docs/api/java/util/concurrent/Executors.html#newFixedThreadPool-int-
-
1
def initialize(num_threads, opts = {})
-
raise ArgumentError.new('number of threads must be greater than zero') if num_threads.to_i < 1
-
defaults = { max_queue: DEFAULT_MAX_QUEUE_SIZE,
-
idletime: DEFAULT_THREAD_IDLETIMEOUT }
-
overrides = { min_threads: num_threads,
-
max_threads: num_threads }
-
super(defaults.merge(opts).merge(overrides))
-
end
-
end
-
end
-
1
require 'concurrent/atomic/event'
-
1
require 'concurrent/executor/abstract_executor_service'
-
1
require 'concurrent/executor/serial_executor_service'
-
-
1
module Concurrent
-
-
# An executor service which runs all operations on the current thread,
-
# blocking as necessary. Operations are performed in the order they are
-
# received and no two operations can be performed simultaneously.
-
#
-
# This executor service exists mainly for testing an debugging. When used
-
# it immediately runs every `#post` operation on the current thread, blocking
-
# that thread until the operation is complete. This can be very beneficial
-
# during testing because it makes all operations deterministic.
-
#
-
# @note Intended for use primarily in testing and debugging.
-
1
class ImmediateExecutor < AbstractExecutorService
-
1
include SerialExecutorService
-
-
# Creates a new executor
-
1
def initialize
-
1
@stopped = Concurrent::Event.new
-
end
-
-
# @!macro executor_service_method_post
-
1
def post(*args, &task)
-
raise ArgumentError.new('no block given') unless block_given?
-
return false unless running?
-
task.call(*args)
-
true
-
end
-
-
# @!macro executor_service_method_left_shift
-
1
def <<(task)
-
post(&task)
-
self
-
end
-
-
# @!macro executor_service_method_running_question
-
1
def running?
-
! shutdown?
-
end
-
-
# @!macro executor_service_method_shuttingdown_question
-
1
def shuttingdown?
-
false
-
end
-
-
# @!macro executor_service_method_shutdown_question
-
1
def shutdown?
-
@stopped.set?
-
end
-
-
# @!macro executor_service_method_shutdown
-
1
def shutdown
-
@stopped.set
-
true
-
end
-
1
alias_method :kill, :shutdown
-
-
# @!macro executor_service_method_wait_for_termination
-
1
def wait_for_termination(timeout = nil)
-
@stopped.wait(timeout)
-
end
-
end
-
end
-
1
require 'concurrent/executor/immediate_executor'
-
1
require 'concurrent/executor/simple_executor_service'
-
-
1
module Concurrent
-
# An executor service which runs all operations on a new thread, blocking
-
# until it completes. Operations are performed in the order they are received
-
# and no two operations can be performed simultaneously.
-
#
-
# This executor service exists mainly for testing an debugging. When used it
-
# immediately runs every `#post` operation on a new thread, blocking the
-
# current thread until the operation is complete. This is similar to how the
-
# ImmediateExecutor works, but the operation has the full stack of the new
-
# thread at its disposal. This can be helpful when the operations will spawn
-
# more operations on the same executor and so on - such a situation might
-
# overflow the single stack in case of an ImmediateExecutor, which is
-
# inconsistent with how it would behave for a threaded executor.
-
#
-
# @note Intended for use primarily in testing and debugging.
-
1
class IndirectImmediateExecutor < ImmediateExecutor
-
# Creates a new executor
-
1
def initialize
-
super
-
@internal_executor = SimpleExecutorService.new
-
end
-
-
# @!macro executor_service_method_post
-
1
def post(*args, &task)
-
raise ArgumentError.new("no block given") unless block_given?
-
return false unless running?
-
-
event = Concurrent::Event.new
-
@internal_executor.post do
-
begin
-
task.call(*args)
-
ensure
-
event.set
-
end
-
end
-
event.wait
-
-
true
-
end
-
end
-
end
-
1
if Concurrent.on_jruby?
-
-
require 'concurrent/executor/java_executor_service'
-
require 'concurrent/executor/serial_executor_service'
-
-
module Concurrent
-
-
# @!macro single_thread_executor
-
# @!macro abstract_executor_service_public_api
-
# @!visibility private
-
class JavaSingleThreadExecutor < JavaExecutorService
-
include SerialExecutorService
-
-
# @!macro single_thread_executor_method_initialize
-
def initialize(opts = {})
-
super(opts)
-
end
-
-
private
-
-
def ns_initialize(opts)
-
@executor = java.util.concurrent.Executors.newSingleThreadExecutor(
-
DaemonThreadFactory.new(ns_auto_terminate?)
-
)
-
@fallback_policy = opts.fetch(:fallback_policy, :discard)
-
raise ArgumentError.new("#{@fallback_policy} is not a valid fallback policy") unless FALLBACK_POLICY_CLASSES.keys.include?(@fallback_policy)
-
end
-
end
-
end
-
end
-
1
if Concurrent.on_jruby?
-
-
require 'concurrent/executor/java_executor_service'
-
-
module Concurrent
-
-
# @!macro thread_pool_executor
-
# @!macro thread_pool_options
-
# @!visibility private
-
class JavaThreadPoolExecutor < JavaExecutorService
-
-
# @!macro thread_pool_executor_constant_default_max_pool_size
-
DEFAULT_MAX_POOL_SIZE = java.lang.Integer::MAX_VALUE # 2147483647
-
-
# @!macro thread_pool_executor_constant_default_min_pool_size
-
DEFAULT_MIN_POOL_SIZE = 0
-
-
# @!macro thread_pool_executor_constant_default_max_queue_size
-
DEFAULT_MAX_QUEUE_SIZE = 0
-
-
# @!macro thread_pool_executor_constant_default_thread_timeout
-
DEFAULT_THREAD_IDLETIMEOUT = 60
-
-
# @!macro thread_pool_executor_attr_reader_max_length
-
attr_reader :max_length
-
-
# @!macro thread_pool_executor_attr_reader_max_queue
-
attr_reader :max_queue
-
-
# @!macro thread_pool_executor_method_initialize
-
def initialize(opts = {})
-
super(opts)
-
end
-
-
# @!macro executor_service_method_can_overflow_question
-
def can_overflow?
-
@max_queue != 0
-
end
-
-
# @!macro thread_pool_executor_attr_reader_min_length
-
def min_length
-
@executor.getCorePoolSize
-
end
-
-
# @!macro thread_pool_executor_attr_reader_max_length
-
def max_length
-
@executor.getMaximumPoolSize
-
end
-
-
# @!macro thread_pool_executor_attr_reader_length
-
def length
-
@executor.getPoolSize
-
end
-
-
# @!macro thread_pool_executor_attr_reader_largest_length
-
def largest_length
-
@executor.getLargestPoolSize
-
end
-
-
# @!macro thread_pool_executor_attr_reader_scheduled_task_count
-
def scheduled_task_count
-
@executor.getTaskCount
-
end
-
-
# @!macro thread_pool_executor_attr_reader_completed_task_count
-
def completed_task_count
-
@executor.getCompletedTaskCount
-
end
-
-
# @!macro thread_pool_executor_attr_reader_idletime
-
def idletime
-
@executor.getKeepAliveTime(java.util.concurrent.TimeUnit::SECONDS)
-
end
-
-
# @!macro thread_pool_executor_attr_reader_queue_length
-
def queue_length
-
@executor.getQueue.size
-
end
-
-
# @!macro thread_pool_executor_attr_reader_remaining_capacity
-
def remaining_capacity
-
@max_queue == 0 ? -1 : @executor.getQueue.remainingCapacity
-
end
-
-
# @!macro executor_service_method_running_question
-
def running?
-
super && !@executor.isTerminating
-
end
-
-
private
-
-
def ns_initialize(opts)
-
min_length = opts.fetch(:min_threads, DEFAULT_MIN_POOL_SIZE).to_i
-
max_length = opts.fetch(:max_threads, DEFAULT_MAX_POOL_SIZE).to_i
-
idletime = opts.fetch(:idletime, DEFAULT_THREAD_IDLETIMEOUT).to_i
-
@max_queue = opts.fetch(:max_queue, DEFAULT_MAX_QUEUE_SIZE).to_i
-
@fallback_policy = opts.fetch(:fallback_policy, :abort)
-
-
raise ArgumentError.new("`max_threads` cannot be less than #{DEFAULT_MIN_POOL_SIZE}") if max_length < DEFAULT_MIN_POOL_SIZE
-
raise ArgumentError.new("`max_threads` cannot be greater than #{DEFAULT_MAX_POOL_SIZE}") if max_length > DEFAULT_MAX_POOL_SIZE
-
raise ArgumentError.new("`min_threads` cannot be less than #{DEFAULT_MIN_POOL_SIZE}") if min_length < DEFAULT_MIN_POOL_SIZE
-
raise ArgumentError.new("`min_threads` cannot be more than `max_threads`") if min_length > max_length
-
raise ArgumentError.new("#{fallback_policy} is not a valid fallback policy") unless FALLBACK_POLICY_CLASSES.include?(@fallback_policy)
-
-
if @max_queue == 0
-
queue = java.util.concurrent.LinkedBlockingQueue.new
-
else
-
queue = java.util.concurrent.LinkedBlockingQueue.new(@max_queue)
-
end
-
-
@executor = java.util.concurrent.ThreadPoolExecutor.new(
-
min_length,
-
max_length,
-
idletime,
-
java.util.concurrent.TimeUnit::SECONDS,
-
queue,
-
DaemonThreadFactory.new(ns_auto_terminate?),
-
FALLBACK_POLICY_CLASSES[@fallback_policy].new)
-
-
end
-
end
-
-
end
-
end
-
1
require 'concurrent/executor/abstract_executor_service'
-
1
require 'concurrent/atomic/event'
-
-
1
module Concurrent
-
-
# @!macro abstract_executor_service_public_api
-
# @!visibility private
-
1
class RubyExecutorService < AbstractExecutorService
-
1
safe_initialization!
-
-
1
def initialize(*args, &block)
-
super
-
@StopEvent = Event.new
-
@StoppedEvent = Event.new
-
end
-
-
1
def post(*args, &task)
-
raise ArgumentError.new('no block given') unless block_given?
-
synchronize do
-
# If the executor is shut down, reject this task
-
return handle_fallback(*args, &task) unless running?
-
ns_execute(*args, &task)
-
true
-
end
-
end
-
-
1
def shutdown
-
synchronize do
-
break unless running?
-
stop_event.set
-
ns_shutdown_execution
-
end
-
true
-
end
-
-
1
def kill
-
synchronize do
-
break if shutdown?
-
stop_event.set
-
ns_kill_execution
-
stopped_event.set
-
end
-
true
-
end
-
-
1
def wait_for_termination(timeout = nil)
-
stopped_event.wait(timeout)
-
end
-
-
1
private
-
-
1
def stop_event
-
@StopEvent
-
end
-
-
1
def stopped_event
-
@StoppedEvent
-
end
-
-
1
def ns_shutdown_execution
-
stopped_event.set
-
end
-
-
1
def ns_running?
-
!stop_event.set?
-
end
-
-
1
def ns_shuttingdown?
-
!(ns_running? || ns_shutdown?)
-
end
-
-
1
def ns_shutdown?
-
stopped_event.set?
-
end
-
end
-
end
-
1
require 'concurrent/executor/ruby_thread_pool_executor'
-
-
1
module Concurrent
-
-
# @!macro single_thread_executor
-
# @!macro abstract_executor_service_public_api
-
# @!visibility private
-
1
class RubySingleThreadExecutor < RubyThreadPoolExecutor
-
-
# @!macro single_thread_executor_method_initialize
-
1
def initialize(opts = {})
-
super(
-
min_threads: 1,
-
max_threads: 1,
-
max_queue: 0,
-
idletime: DEFAULT_THREAD_IDLETIMEOUT,
-
fallback_policy: opts.fetch(:fallback_policy, :discard),
-
)
-
end
-
end
-
end
-
1
require 'thread'
-
1
require 'concurrent/atomic/event'
-
1
require 'concurrent/concern/logging'
-
1
require 'concurrent/executor/ruby_executor_service'
-
1
require 'concurrent/utility/monotonic_time'
-
-
1
module Concurrent
-
-
# @!macro thread_pool_executor
-
# @!macro thread_pool_options
-
# @!visibility private
-
1
class RubyThreadPoolExecutor < RubyExecutorService
-
-
# @!macro thread_pool_executor_constant_default_max_pool_size
-
1
DEFAULT_MAX_POOL_SIZE = 2_147_483_647 # java.lang.Integer::MAX_VALUE
-
-
# @!macro thread_pool_executor_constant_default_min_pool_size
-
1
DEFAULT_MIN_POOL_SIZE = 0
-
-
# @!macro thread_pool_executor_constant_default_max_queue_size
-
1
DEFAULT_MAX_QUEUE_SIZE = 0
-
-
# @!macro thread_pool_executor_constant_default_thread_timeout
-
1
DEFAULT_THREAD_IDLETIMEOUT = 60
-
-
# @!macro thread_pool_executor_attr_reader_max_length
-
1
attr_reader :max_length
-
-
# @!macro thread_pool_executor_attr_reader_min_length
-
1
attr_reader :min_length
-
-
# @!macro thread_pool_executor_attr_reader_idletime
-
1
attr_reader :idletime
-
-
# @!macro thread_pool_executor_attr_reader_max_queue
-
1
attr_reader :max_queue
-
-
# @!macro thread_pool_executor_method_initialize
-
1
def initialize(opts = {})
-
super(opts)
-
end
-
-
# @!macro thread_pool_executor_attr_reader_largest_length
-
1
def largest_length
-
synchronize { @largest_length }
-
end
-
-
# @!macro thread_pool_executor_attr_reader_scheduled_task_count
-
1
def scheduled_task_count
-
synchronize { @scheduled_task_count }
-
end
-
-
# @!macro thread_pool_executor_attr_reader_completed_task_count
-
1
def completed_task_count
-
synchronize { @completed_task_count }
-
end
-
-
# @!macro executor_service_method_can_overflow_question
-
1
def can_overflow?
-
synchronize { ns_limited_queue? }
-
end
-
-
# @!macro thread_pool_executor_attr_reader_length
-
1
def length
-
synchronize { @pool.length }
-
end
-
-
# @!macro thread_pool_executor_attr_reader_queue_length
-
1
def queue_length
-
synchronize { @queue.length }
-
end
-
-
# @!macro thread_pool_executor_attr_reader_remaining_capacity
-
1
def remaining_capacity
-
synchronize do
-
if ns_limited_queue?
-
@max_queue - @queue.length
-
else
-
-1
-
end
-
end
-
end
-
-
# @!visibility private
-
1
def remove_busy_worker(worker)
-
synchronize { ns_remove_busy_worker worker }
-
end
-
-
# @!visibility private
-
1
def ready_worker(worker)
-
synchronize { ns_ready_worker worker }
-
end
-
-
# @!visibility private
-
1
def worker_not_old_enough(worker)
-
synchronize { ns_worker_not_old_enough worker }
-
end
-
-
# @!visibility private
-
1
def worker_died(worker)
-
synchronize { ns_worker_died worker }
-
end
-
-
# @!visibility private
-
1
def worker_task_completed
-
synchronize { @completed_task_count += 1 }
-
end
-
-
1
private
-
-
# @!visibility private
-
1
def ns_initialize(opts)
-
@min_length = opts.fetch(:min_threads, DEFAULT_MIN_POOL_SIZE).to_i
-
@max_length = opts.fetch(:max_threads, DEFAULT_MAX_POOL_SIZE).to_i
-
@idletime = opts.fetch(:idletime, DEFAULT_THREAD_IDLETIMEOUT).to_i
-
@max_queue = opts.fetch(:max_queue, DEFAULT_MAX_QUEUE_SIZE).to_i
-
@fallback_policy = opts.fetch(:fallback_policy, :abort)
-
raise ArgumentError.new("#{@fallback_policy} is not a valid fallback policy") unless FALLBACK_POLICIES.include?(@fallback_policy)
-
-
raise ArgumentError.new("`max_threads` cannot be less than #{DEFAULT_MIN_POOL_SIZE}") if @max_length < DEFAULT_MIN_POOL_SIZE
-
raise ArgumentError.new("`max_threads` cannot be greater than #{DEFAULT_MAX_POOL_SIZE}") if @max_length > DEFAULT_MAX_POOL_SIZE
-
raise ArgumentError.new("`min_threads` cannot be less than #{DEFAULT_MIN_POOL_SIZE}") if @min_length < DEFAULT_MIN_POOL_SIZE
-
raise ArgumentError.new("`min_threads` cannot be more than `max_threads`") if min_length > max_length
-
-
@pool = [] # all workers
-
@ready = [] # used as a stash (most idle worker is at the start)
-
@queue = [] # used as queue
-
# @ready or @queue is empty at all times
-
@scheduled_task_count = 0
-
@completed_task_count = 0
-
@largest_length = 0
-
@workers_counter = 0
-
@ruby_pid = $$ # detects if Ruby has forked
-
-
@gc_interval = opts.fetch(:gc_interval, @idletime / 2.0).to_i # undocumented
-
@next_gc_time = Concurrent.monotonic_time + @gc_interval
-
end
-
-
# @!visibility private
-
1
def ns_limited_queue?
-
@max_queue != 0
-
end
-
-
# @!visibility private
-
1
def ns_execute(*args, &task)
-
ns_reset_if_forked
-
-
if ns_assign_worker(*args, &task) || ns_enqueue(*args, &task)
-
@scheduled_task_count += 1
-
else
-
handle_fallback(*args, &task)
-
end
-
-
ns_prune_pool if @next_gc_time < Concurrent.monotonic_time
-
end
-
-
# @!visibility private
-
1
def ns_shutdown_execution
-
ns_reset_if_forked
-
-
if @pool.empty?
-
# nothing to do
-
stopped_event.set
-
end
-
-
if @queue.empty?
-
# no more tasks will be accepted, just stop all workers
-
@pool.each(&:stop)
-
end
-
end
-
-
# @!visibility private
-
1
def ns_kill_execution
-
# TODO log out unprocessed tasks in queue
-
# TODO try to shutdown first?
-
@pool.each(&:kill)
-
@pool.clear
-
@ready.clear
-
end
-
-
# tries to assign task to a worker, tries to get one from @ready or to create new one
-
# @return [true, false] if task is assigned to a worker
-
#
-
# @!visibility private
-
1
def ns_assign_worker(*args, &task)
-
# keep growing if the pool is not at the minimum yet
-
worker = (@ready.pop if @pool.size >= @min_length) || ns_add_busy_worker
-
if worker
-
worker << [task, args]
-
true
-
else
-
false
-
end
-
rescue ThreadError
-
# Raised when the operating system refuses to create the new thread
-
return false
-
end
-
-
# tries to enqueue task
-
# @return [true, false] if enqueued
-
#
-
# @!visibility private
-
1
def ns_enqueue(*args, &task)
-
if !ns_limited_queue? || @queue.size < @max_queue
-
@queue << [task, args]
-
true
-
else
-
false
-
end
-
end
-
-
# @!visibility private
-
1
def ns_worker_died(worker)
-
ns_remove_busy_worker worker
-
replacement_worker = ns_add_busy_worker
-
ns_ready_worker replacement_worker, false if replacement_worker
-
end
-
-
# creates new worker which has to receive work to do after it's added
-
# @return [nil, Worker] nil of max capacity is reached
-
#
-
# @!visibility private
-
1
def ns_add_busy_worker
-
return if @pool.size >= @max_length
-
-
@workers_counter += 1
-
@pool << (worker = Worker.new(self, @workers_counter))
-
@largest_length = @pool.length if @pool.length > @largest_length
-
worker
-
end
-
-
# handle ready worker, giving it new job or assigning back to @ready
-
#
-
# @!visibility private
-
1
def ns_ready_worker(worker, success = true)
-
task_and_args = @queue.shift
-
if task_and_args
-
worker << task_and_args
-
else
-
# stop workers when !running?, do not return them to @ready
-
if running?
-
@ready.push(worker)
-
else
-
worker.stop
-
end
-
end
-
end
-
-
# returns back worker to @ready which was not idle for enough time
-
#
-
# @!visibility private
-
1
def ns_worker_not_old_enough(worker)
-
# let's put workers coming from idle_test back to the start (as the oldest worker)
-
@ready.unshift(worker)
-
true
-
end
-
-
# removes a worker which is not in not tracked in @ready
-
#
-
# @!visibility private
-
1
def ns_remove_busy_worker(worker)
-
@pool.delete(worker)
-
stopped_event.set if @pool.empty? && !running?
-
true
-
end
-
-
# try oldest worker if it is idle for enough time, it's returned back at the start
-
#
-
# @!visibility private
-
1
def ns_prune_pool
-
return if @pool.size <= @min_length
-
-
last_used = @ready.shift
-
last_used << :idle_test if last_used
-
-
@next_gc_time = Concurrent.monotonic_time + @gc_interval
-
end
-
-
1
def ns_reset_if_forked
-
if $$ != @ruby_pid
-
@queue.clear
-
@ready.clear
-
@pool.clear
-
@scheduled_task_count = 0
-
@completed_task_count = 0
-
@largest_length = 0
-
@workers_counter = 0
-
@ruby_pid = $$
-
end
-
end
-
-
# @!visibility private
-
1
class Worker
-
1
include Concern::Logging
-
-
1
def initialize(pool, id)
-
# instance variables accessed only under pool's lock so no need to sync here again
-
@queue = Queue.new
-
@pool = pool
-
@thread = create_worker @queue, pool, pool.idletime
-
-
if @thread.respond_to?(:name=)
-
@thread.name = [pool.name, 'worker', id].compact.join('-')
-
end
-
end
-
-
1
def <<(message)
-
@queue << message
-
end
-
-
1
def stop
-
@queue << :stop
-
end
-
-
1
def kill
-
@thread.kill
-
end
-
-
1
private
-
-
1
def create_worker(queue, pool, idletime)
-
Thread.new(queue, pool, idletime) do |my_queue, my_pool, my_idletime|
-
last_message = Concurrent.monotonic_time
-
catch(:stop) do
-
loop do
-
-
case message = my_queue.pop
-
when :idle_test
-
if (Concurrent.monotonic_time - last_message) > my_idletime
-
my_pool.remove_busy_worker(self)
-
throw :stop
-
else
-
my_pool.worker_not_old_enough(self)
-
end
-
-
when :stop
-
my_pool.remove_busy_worker(self)
-
throw :stop
-
-
else
-
task, args = message
-
run_task my_pool, task, args
-
last_message = Concurrent.monotonic_time
-
-
my_pool.ready_worker(self)
-
end
-
end
-
end
-
end
-
end
-
-
1
def run_task(pool, task, args)
-
task.call(*args)
-
pool.worker_task_completed
-
rescue => ex
-
# let it fail
-
log DEBUG, ex
-
rescue Exception => ex
-
log ERROR, ex
-
pool.worker_died(self)
-
throw :stop
-
end
-
end
-
-
1
private_constant :Worker
-
end
-
end
-
1
require 'concurrent/synchronization'
-
-
1
module Concurrent
-
-
# A simple utility class that executes a callable and returns and array of three elements:
-
# success - indicating if the callable has been executed without errors
-
# value - filled by the callable result if it has been executed without errors, nil otherwise
-
# reason - the error risen by the callable if it has been executed with errors, nil otherwise
-
1
class SafeTaskExecutor < Synchronization::LockableObject
-
-
1
def initialize(task, opts = {})
-
@task = task
-
@exception_class = opts.fetch(:rescue_exception, false) ? Exception : StandardError
-
super() # ensures visibility
-
end
-
-
# @return [Array]
-
1
def execute(*args)
-
synchronize do
-
success = false
-
value = reason = nil
-
-
begin
-
value = @task.call(*args)
-
success = true
-
rescue @exception_class => ex
-
reason = ex
-
success = false
-
end
-
-
[success, value, reason]
-
end
-
end
-
end
-
end
-
1
require 'concurrent/executor/executor_service'
-
-
1
module Concurrent
-
-
# Indicates that the including `ExecutorService` guarantees
-
# that all operations will occur in the order they are post and that no
-
# two operations may occur simultaneously. This module provides no
-
# functionality and provides no guarantees. That is the responsibility
-
# of the including class. This module exists solely to allow the including
-
# object to be interrogated for its serialization status.
-
#
-
# @example
-
# class Foo
-
# include Concurrent::SerialExecutor
-
# end
-
#
-
# foo = Foo.new
-
#
-
# foo.is_a? Concurrent::ExecutorService #=> true
-
# foo.is_a? Concurrent::SerialExecutor #=> true
-
# foo.serialized? #=> true
-
#
-
# @!visibility private
-
1
module SerialExecutorService
-
1
include ExecutorService
-
-
# @!macro executor_service_method_serialized_question
-
#
-
# @note Always returns `true`
-
1
def serialized?
-
true
-
end
-
end
-
end
-
1
require 'concurrent/errors'
-
1
require 'concurrent/concern/logging'
-
1
require 'concurrent/synchronization'
-
-
1
module Concurrent
-
-
# Ensures passed jobs in a serialized order never running at the same time.
-
1
class SerializedExecution < Synchronization::LockableObject
-
1
include Concern::Logging
-
-
1
def initialize()
-
super()
-
synchronize { ns_initialize }
-
end
-
-
1
Job = Struct.new(:executor, :args, :block) do
-
1
def call
-
block.call(*args)
-
end
-
end
-
-
# Submit a task to the executor for asynchronous processing.
-
#
-
# @param [Executor] executor to be used for this job
-
#
-
# @param [Array] args zero or more arguments to be passed to the task
-
#
-
# @yield the asynchronous task to perform
-
#
-
# @return [Boolean] `true` if the task is queued, `false` if the executor
-
# is not running
-
#
-
# @raise [ArgumentError] if no task is given
-
1
def post(executor, *args, &task)
-
posts [[executor, args, task]]
-
true
-
end
-
-
# As {#post} but allows to submit multiple tasks at once, it's guaranteed that they will not
-
# be interleaved by other tasks.
-
#
-
# @param [Array<Array(ExecutorService, Array<Object>, Proc)>] posts array of triplets where
-
# first is a {ExecutorService}, second is array of args for task, third is a task (Proc)
-
1
def posts(posts)
-
# if can_overflow?
-
# raise ArgumentError, 'SerializedExecution does not support thread-pools which can overflow'
-
# end
-
-
return nil if posts.empty?
-
-
jobs = posts.map { |executor, args, task| Job.new executor, args, task }
-
-
job_to_post = synchronize do
-
if @being_executed
-
@stash.push(*jobs)
-
nil
-
else
-
@being_executed = true
-
@stash.push(*jobs[1..-1])
-
jobs.first
-
end
-
end
-
-
call_job job_to_post if job_to_post
-
true
-
end
-
-
1
private
-
-
1
def ns_initialize
-
@being_executed = false
-
@stash = []
-
end
-
-
1
def call_job(job)
-
did_it_run = begin
-
job.executor.post { work(job) }
-
true
-
rescue RejectedExecutionError => ex
-
false
-
end
-
-
# TODO not the best idea to run it myself
-
unless did_it_run
-
begin
-
work job
-
rescue => ex
-
# let it fail
-
log DEBUG, ex
-
end
-
end
-
end
-
-
# ensures next job is executed if any is stashed
-
1
def work(job)
-
job.call
-
ensure
-
synchronize do
-
job = @stash.shift || (@being_executed = false)
-
end
-
-
# TODO maybe be able to tell caching pool to just enqueue this job, because the current one end at the end
-
# of this block
-
call_job job if job
-
end
-
end
-
end
-
1
require 'delegate'
-
1
require 'concurrent/executor/serial_executor_service'
-
1
require 'concurrent/executor/serialized_execution'
-
-
1
module Concurrent
-
-
# A wrapper/delegator for any `ExecutorService` that
-
# guarantees serialized execution of tasks.
-
#
-
# @see [SimpleDelegator](http://www.ruby-doc.org/stdlib-2.1.2/libdoc/delegate/rdoc/SimpleDelegator.html)
-
# @see Concurrent::SerializedExecution
-
1
class SerializedExecutionDelegator < SimpleDelegator
-
1
include SerialExecutorService
-
-
1
def initialize(executor)
-
@executor = executor
-
@serializer = SerializedExecution.new
-
super(executor)
-
end
-
-
# @!macro executor_service_method_post
-
1
def post(*args, &task)
-
raise ArgumentError.new('no block given') unless block_given?
-
return false unless running?
-
@serializer.post(@executor, *args, &task)
-
end
-
end
-
end
-
1
require 'concurrent/atomics'
-
1
require 'concurrent/executor/executor_service'
-
-
1
module Concurrent
-
-
# An executor service in which every operation spawns a new,
-
# independently operating thread.
-
#
-
# This is perhaps the most inefficient executor service in this
-
# library. It exists mainly for testing an debugging. Thread creation
-
# and management is expensive in Ruby and this executor performs no
-
# resource pooling. This can be very beneficial during testing and
-
# debugging because it decouples the using code from the underlying
-
# executor implementation. In production this executor will likely
-
# lead to suboptimal performance.
-
#
-
# @note Intended for use primarily in testing and debugging.
-
1
class SimpleExecutorService < RubyExecutorService
-
-
# @!macro executor_service_method_post
-
1
def self.post(*args)
-
raise ArgumentError.new('no block given') unless block_given?
-
Thread.new(*args) do
-
Thread.current.abort_on_exception = false
-
yield(*args)
-
end
-
true
-
end
-
-
# @!macro executor_service_method_left_shift
-
1
def self.<<(task)
-
post(&task)
-
self
-
end
-
-
# @!macro executor_service_method_post
-
1
def post(*args, &task)
-
raise ArgumentError.new('no block given') unless block_given?
-
return false unless running?
-
@count.increment
-
Thread.new(*args) do
-
Thread.current.abort_on_exception = false
-
begin
-
yield(*args)
-
ensure
-
@count.decrement
-
@stopped.set if @running.false? && @count.value == 0
-
end
-
end
-
end
-
-
# @!macro executor_service_method_left_shift
-
1
def <<(task)
-
post(&task)
-
self
-
end
-
-
# @!macro executor_service_method_running_question
-
1
def running?
-
@running.true?
-
end
-
-
# @!macro executor_service_method_shuttingdown_question
-
1
def shuttingdown?
-
@running.false? && ! @stopped.set?
-
end
-
-
# @!macro executor_service_method_shutdown_question
-
1
def shutdown?
-
@stopped.set?
-
end
-
-
# @!macro executor_service_method_shutdown
-
1
def shutdown
-
@running.make_false
-
@stopped.set if @count.value == 0
-
true
-
end
-
-
# @!macro executor_service_method_kill
-
1
def kill
-
@running.make_false
-
@stopped.set
-
true
-
end
-
-
# @!macro executor_service_method_wait_for_termination
-
1
def wait_for_termination(timeout = nil)
-
@stopped.wait(timeout)
-
end
-
-
1
private
-
-
1
def ns_initialize(*args)
-
@running = Concurrent::AtomicBoolean.new(true)
-
@stopped = Concurrent::Event.new
-
@count = Concurrent::AtomicFixnum.new(0)
-
end
-
end
-
end
-
1
require 'concurrent/utility/engine'
-
1
require 'concurrent/executor/ruby_single_thread_executor'
-
-
1
module Concurrent
-
-
1
if Concurrent.on_jruby?
-
require 'concurrent/executor/java_single_thread_executor'
-
end
-
-
SingleThreadExecutorImplementation = case
-
1
when Concurrent.on_jruby?
-
JavaSingleThreadExecutor
-
else
-
1
RubySingleThreadExecutor
-
end
-
1
private_constant :SingleThreadExecutorImplementation
-
-
# @!macro single_thread_executor
-
#
-
# A thread pool with a single thread an unlimited queue. Should the thread
-
# die for any reason it will be removed and replaced, thus ensuring that
-
# the executor will always remain viable and available to process jobs.
-
#
-
# A common pattern for background processing is to create a single thread
-
# on which an infinite loop is run. The thread's loop blocks on an input
-
# source (perhaps blocking I/O or a queue) and processes each input as it
-
# is received. This pattern has several issues. The thread itself is highly
-
# susceptible to errors during processing. Also, the thread itself must be
-
# constantly monitored and restarted should it die. `SingleThreadExecutor`
-
# encapsulates all these bahaviors. The task processor is highly resilient
-
# to errors from within tasks. Also, should the thread die it will
-
# automatically be restarted.
-
#
-
# The API and behavior of this class are based on Java's `SingleThreadExecutor`.
-
#
-
# @!macro abstract_executor_service_public_api
-
1
class SingleThreadExecutor < SingleThreadExecutorImplementation
-
-
# @!macro single_thread_executor_method_initialize
-
#
-
# Create a new thread pool.
-
#
-
# @option opts [Symbol] :fallback_policy (:discard) the policy for handling new
-
# tasks that are received when the queue size has reached
-
# `max_queue` or the executor has shut down
-
#
-
# @raise [ArgumentError] if `:fallback_policy` is not one of the values specified
-
# in `FALLBACK_POLICIES`
-
#
-
# @see http://docs.oracle.com/javase/tutorial/essential/concurrency/pools.html
-
# @see http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/Executors.html
-
# @see http://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ExecutorService.html
-
-
# @!method initialize(opts = {})
-
# @!macro single_thread_executor_method_initialize
-
end
-
end
-
1
require 'concurrent/utility/engine'
-
1
require 'concurrent/executor/ruby_thread_pool_executor'
-
-
1
module Concurrent
-
-
1
if Concurrent.on_jruby?
-
require 'concurrent/executor/java_thread_pool_executor'
-
end
-
-
ThreadPoolExecutorImplementation = case
-
1
when Concurrent.on_jruby?
-
JavaThreadPoolExecutor
-
else
-
1
RubyThreadPoolExecutor
-
end
-
1
private_constant :ThreadPoolExecutorImplementation
-
-
# @!macro thread_pool_executor
-
#
-
# An abstraction composed of one or more threads and a task queue. Tasks
-
# (blocks or `proc` objects) are submitted to the pool and added to the queue.
-
# The threads in the pool remove the tasks and execute them in the order
-
# they were received.
-
#
-
# A `ThreadPoolExecutor` will automatically adjust the pool size according
-
# to the bounds set by `min-threads` and `max-threads`. When a new task is
-
# submitted and fewer than `min-threads` threads are running, a new thread
-
# is created to handle the request, even if other worker threads are idle.
-
# If there are more than `min-threads` but less than `max-threads` threads
-
# running, a new thread will be created only if the queue is full.
-
#
-
# Threads that are idle for too long will be garbage collected, down to the
-
# configured minimum options. Should a thread crash it, too, will be garbage collected.
-
#
-
# `ThreadPoolExecutor` is based on the Java class of the same name. From
-
# the official Java documentation;
-
#
-
# > Thread pools address two different problems: they usually provide
-
# > improved performance when executing large numbers of asynchronous tasks,
-
# > due to reduced per-task invocation overhead, and they provide a means
-
# > of bounding and managing the resources, including threads, consumed
-
# > when executing a collection of tasks. Each ThreadPoolExecutor also
-
# > maintains some basic statistics, such as the number of completed tasks.
-
# >
-
# > To be useful across a wide range of contexts, this class provides many
-
# > adjustable parameters and extensibility hooks. However, programmers are
-
# > urged to use the more convenient Executors factory methods
-
# > [CachedThreadPool] (unbounded thread pool, with automatic thread reclamation),
-
# > [FixedThreadPool] (fixed size thread pool) and [SingleThreadExecutor] (single
-
# > background thread), that preconfigure settings for the most common usage
-
# > scenarios.
-
#
-
# @!macro thread_pool_options
-
#
-
# @!macro thread_pool_executor_public_api
-
1
class ThreadPoolExecutor < ThreadPoolExecutorImplementation
-
-
# @!macro thread_pool_executor_method_initialize
-
#
-
# Create a new thread pool.
-
#
-
# @param [Hash] opts the options which configure the thread pool.
-
#
-
# @option opts [Integer] :max_threads (DEFAULT_MAX_POOL_SIZE) the maximum
-
# number of threads to be created
-
# @option opts [Integer] :min_threads (DEFAULT_MIN_POOL_SIZE) When a new task is submitted
-
# and fewer than `min_threads` are running, a new thread is created
-
# @option opts [Integer] :idletime (DEFAULT_THREAD_IDLETIMEOUT) the maximum
-
# number of seconds a thread may be idle before being reclaimed
-
# @option opts [Integer] :max_queue (DEFAULT_MAX_QUEUE_SIZE) the maximum
-
# number of tasks allowed in the work queue at any one time; a value of
-
# zero means the queue may grow without bound
-
# @option opts [Symbol] :fallback_policy (:abort) the policy for handling new
-
# tasks that are received when the queue size has reached
-
# `max_queue` or the executor has shut down
-
#
-
# @raise [ArgumentError] if `:max_threads` is less than one
-
# @raise [ArgumentError] if `:min_threads` is less than zero
-
# @raise [ArgumentError] if `:fallback_policy` is not one of the values specified
-
# in `FALLBACK_POLICIES`
-
#
-
# @see http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/ThreadPoolExecutor.html
-
-
# @!method initialize(opts = {})
-
# @!macro thread_pool_executor_method_initialize
-
end
-
end
-
1
require 'concurrent/scheduled_task'
-
1
require 'concurrent/atomic/event'
-
1
require 'concurrent/collection/non_concurrent_priority_queue'
-
1
require 'concurrent/executor/executor_service'
-
1
require 'concurrent/executor/single_thread_executor'
-
-
1
require 'concurrent/options'
-
-
1
module Concurrent
-
-
# Executes a collection of tasks, each after a given delay. A master task
-
# monitors the set and schedules each task for execution at the appropriate
-
# time. Tasks are run on the global thread pool or on the supplied executor.
-
# Each task is represented as a `ScheduledTask`.
-
#
-
# @see Concurrent::ScheduledTask
-
#
-
# @!macro monotonic_clock_warning
-
1
class TimerSet < RubyExecutorService
-
-
# Create a new set of timed tasks.
-
#
-
# @!macro executor_options
-
#
-
# @param [Hash] opts the options used to specify the executor on which to perform actions
-
# @option opts [Executor] :executor when set use the given `Executor` instance.
-
# Three special values are also supported: `:task` returns the global task pool,
-
# `:operation` returns the global operation pool, and `:immediate` returns a new
-
# `ImmediateExecutor` object.
-
1
def initialize(opts = {})
-
super(opts)
-
end
-
-
# Post a task to be execute run after a given delay (in seconds). If the
-
# delay is less than 1/100th of a second the task will be immediately post
-
# to the executor.
-
#
-
# @param [Float] delay the number of seconds to wait for before executing the task.
-
# @param [Array<Object>] args the arguments passed to the task on execution.
-
#
-
# @yield the task to be performed.
-
#
-
# @return [Concurrent::ScheduledTask, false] IVar representing the task if the post
-
# is successful; false after shutdown.
-
#
-
# @raise [ArgumentError] if the intended execution time is not in the future.
-
# @raise [ArgumentError] if no block is given.
-
1
def post(delay, *args, &task)
-
raise ArgumentError.new('no block given') unless block_given?
-
return false unless running?
-
opts = { executor: @task_executor,
-
args: args,
-
timer_set: self }
-
task = ScheduledTask.execute(delay, opts, &task) # may raise exception
-
task.unscheduled? ? false : task
-
end
-
-
# Begin an immediate shutdown. In-progress tasks will be allowed to
-
# complete but enqueued tasks will be dismissed and no new tasks
-
# will be accepted. Has no additional effect if the thread pool is
-
# not running.
-
1
def kill
-
shutdown
-
end
-
-
1
private :<<
-
-
1
private
-
-
# Initialize the object.
-
#
-
# @param [Hash] opts the options to create the object with.
-
# @!visibility private
-
1
def ns_initialize(opts)
-
@queue = Collection::NonConcurrentPriorityQueue.new(order: :min)
-
@task_executor = Options.executor_from_options(opts) || Concurrent.global_io_executor
-
@timer_executor = SingleThreadExecutor.new
-
@condition = Event.new
-
@ruby_pid = $$ # detects if Ruby has forked
-
end
-
-
# Post the task to the internal queue.
-
#
-
# @note This is intended as a callback method from ScheduledTask
-
# only. It is not intended to be used directly. Post a task
-
# by using the `SchedulesTask#execute` method.
-
#
-
# @!visibility private
-
1
def post_task(task)
-
synchronize { ns_post_task(task) }
-
end
-
-
# @!visibility private
-
1
def ns_post_task(task)
-
return false unless ns_running?
-
ns_reset_if_forked
-
if (task.initial_delay) <= 0.01
-
task.executor.post { task.process_task }
-
else
-
@queue.push(task)
-
# only post the process method when the queue is empty
-
@timer_executor.post(&method(:process_tasks)) if @queue.length == 1
-
@condition.set
-
end
-
true
-
end
-
-
# Remove the given task from the queue.
-
#
-
# @note This is intended as a callback method from `ScheduledTask`
-
# only. It is not intended to be used directly. Cancel a task
-
# by using the `ScheduledTask#cancel` method.
-
#
-
# @!visibility private
-
1
def remove_task(task)
-
synchronize { @queue.delete(task) }
-
end
-
-
# `ExecutorService` callback called during shutdown.
-
#
-
# @!visibility private
-
1
def ns_shutdown_execution
-
ns_reset_if_forked
-
@queue.clear
-
@timer_executor.kill
-
stopped_event.set
-
end
-
-
1
def ns_reset_if_forked
-
if $$ != @ruby_pid
-
@queue.clear
-
@condition.reset
-
@ruby_pid = $$
-
end
-
end
-
-
# Run a loop and execute tasks in the scheduled order and at the approximate
-
# scheduled time. If no tasks remain the thread will exit gracefully so that
-
# garbage collection can occur. If there are no ready tasks it will sleep
-
# for up to 60 seconds waiting for the next scheduled task.
-
#
-
# @!visibility private
-
1
def process_tasks
-
loop do
-
task = synchronize { @condition.reset; @queue.peek }
-
break unless task
-
-
now = Concurrent.monotonic_time
-
diff = task.schedule_time - now
-
-
if diff <= 0
-
# We need to remove the task from the queue before passing
-
# it to the executor, to avoid race conditions where we pass
-
# the peek'ed task to the executor and then pop a different
-
# one that's been added in the meantime.
-
#
-
# Note that there's no race condition between the peek and
-
# this pop - this pop could retrieve a different task from
-
# the peek, but that task would be due to fire now anyway
-
# (because @queue is a priority queue, and this thread is
-
# the only reader, so whatever timer is at the head of the
-
# queue now must have the same pop time, or a closer one, as
-
# when we peeked).
-
task = synchronize { @queue.pop }
-
task.executor.post { task.process_task }
-
else
-
@condition.wait([diff, 60].min)
-
end
-
end
-
end
-
end
-
end
-
1
require 'concurrent/executor/abstract_executor_service'
-
1
require 'concurrent/executor/cached_thread_pool'
-
1
require 'concurrent/executor/executor_service'
-
1
require 'concurrent/executor/fixed_thread_pool'
-
1
require 'concurrent/executor/immediate_executor'
-
1
require 'concurrent/executor/indirect_immediate_executor'
-
1
require 'concurrent/executor/java_executor_service'
-
1
require 'concurrent/executor/java_single_thread_executor'
-
1
require 'concurrent/executor/java_thread_pool_executor'
-
1
require 'concurrent/executor/ruby_executor_service'
-
1
require 'concurrent/executor/ruby_single_thread_executor'
-
1
require 'concurrent/executor/ruby_thread_pool_executor'
-
1
require 'concurrent/executor/cached_thread_pool'
-
1
require 'concurrent/executor/safe_task_executor'
-
1
require 'concurrent/executor/serial_executor_service'
-
1
require 'concurrent/executor/serialized_execution'
-
1
require 'concurrent/executor/serialized_execution_delegator'
-
1
require 'concurrent/executor/single_thread_executor'
-
1
require 'concurrent/executor/thread_pool_executor'
-
1
require 'concurrent/executor/timer_set'
-
1
require 'thread'
-
1
require 'concurrent/constants'
-
1
require 'concurrent/errors'
-
1
require 'concurrent/ivar'
-
1
require 'concurrent/executor/safe_task_executor'
-
-
1
require 'concurrent/options'
-
-
# TODO (pitr-ch 14-Mar-2017): deprecate, Future, Promise, etc.
-
-
-
1
module Concurrent
-
-
# {include:file:docs-source/future.md}
-
#
-
# @!macro copy_options
-
#
-
# @see http://ruby-doc.org/stdlib-2.1.1/libdoc/observer/rdoc/Observable.html Ruby Observable module
-
# @see http://clojuredocs.org/clojure_core/clojure.core/future Clojure's future function
-
# @see http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/Future.html java.util.concurrent.Future
-
1
class Future < IVar
-
-
# Create a new `Future` in the `:unscheduled` state.
-
#
-
# @yield the asynchronous operation to perform
-
#
-
# @!macro executor_and_deref_options
-
#
-
# @option opts [object, Array] :args zero or more arguments to be passed the task
-
# block on execution
-
#
-
# @raise [ArgumentError] if no block is given
-
1
def initialize(opts = {}, &block)
-
raise ArgumentError.new('no block given') unless block_given?
-
super(NULL, opts.merge(__task_from_block__: block), &nil)
-
end
-
-
# Execute an `:unscheduled` `Future`. Immediately sets the state to `:pending` and
-
# passes the block to a new thread/thread pool for eventual execution.
-
# Does nothing if the `Future` is in any state other than `:unscheduled`.
-
#
-
# @return [Future] a reference to `self`
-
#
-
# @example Instance and execute in separate steps
-
# future = Concurrent::Future.new{ sleep(1); 42 }
-
# future.state #=> :unscheduled
-
# future.execute
-
# future.state #=> :pending
-
#
-
# @example Instance and execute in one line
-
# future = Concurrent::Future.new{ sleep(1); 42 }.execute
-
# future.state #=> :pending
-
1
def execute
-
if compare_and_set_state(:pending, :unscheduled)
-
@executor.post{ safe_execute(@task, @args) }
-
self
-
end
-
end
-
-
# Create a new `Future` object with the given block, execute it, and return the
-
# `:pending` object.
-
#
-
# @yield the asynchronous operation to perform
-
#
-
# @!macro executor_and_deref_options
-
#
-
# @option opts [object, Array] :args zero or more arguments to be passed the task
-
# block on execution
-
#
-
# @raise [ArgumentError] if no block is given
-
#
-
# @return [Future] the newly created `Future` in the `:pending` state
-
#
-
# @example
-
# future = Concurrent::Future.execute{ sleep(1); 42 }
-
# future.state #=> :pending
-
1
def self.execute(opts = {}, &block)
-
Future.new(opts, &block).execute
-
end
-
-
# @!macro ivar_set_method
-
1
def set(value = NULL, &block)
-
check_for_block_or_value!(block_given?, value)
-
synchronize do
-
if @state != :unscheduled
-
raise MultipleAssignmentError
-
else
-
@task = block || Proc.new { value }
-
end
-
end
-
execute
-
end
-
-
# Attempt to cancel the operation if it has not already processed.
-
# The operation can only be cancelled while still `pending`. It cannot
-
# be cancelled once it has begun processing or has completed.
-
#
-
# @return [Boolean] was the operation successfully cancelled.
-
1
def cancel
-
if compare_and_set_state(:cancelled, :pending)
-
complete(false, nil, CancelledOperationError.new)
-
true
-
else
-
false
-
end
-
end
-
-
# Has the operation been successfully cancelled?
-
#
-
# @return [Boolean]
-
1
def cancelled?
-
state == :cancelled
-
end
-
-
# Wait the given number of seconds for the operation to complete.
-
# On timeout attempt to cancel the operation.
-
#
-
# @param [Numeric] timeout the maximum time in seconds to wait.
-
# @return [Boolean] true if the operation completed before the timeout
-
# else false
-
1
def wait_or_cancel(timeout)
-
wait(timeout)
-
if complete?
-
true
-
else
-
cancel
-
false
-
end
-
end
-
-
1
protected
-
-
1
def ns_initialize(value, opts)
-
super
-
@state = :unscheduled
-
@task = opts[:__task_from_block__]
-
@executor = Options.executor_from_options(opts) || Concurrent.global_io_executor
-
@args = get_arguments_from(opts)
-
end
-
end
-
end
-
1
require 'concurrent/utility/engine'
-
1
require 'concurrent/thread_safe/util'
-
-
1
module Concurrent
-
-
# @!macro concurrent_hash
-
#
-
# A thread-safe subclass of Hash. This version locks against the object
-
# itself for every method call, ensuring only one thread can be reading
-
# or writing at a time. This includes iteration methods like `#each`,
-
# which takes the lock repeatedly when reading an item.
-
#
-
# @see http://ruby-doc.org/core-2.2.0/Hash.html Ruby standard library `Hash`
-
-
# @!macro internal_implementation_note
-
HashImplementation = case
-
1
when Concurrent.on_cruby?
-
# Hash is thread-safe in practice because CRuby runs
-
# threads one at a time and does not do context
-
# switching during the execution of C functions.
-
1
::Hash
-
-
when Concurrent.on_jruby?
-
require 'jruby/synchronized'
-
-
class JRubyHash < ::Hash
-
include JRuby::Synchronized
-
end
-
JRubyHash
-
-
when Concurrent.on_rbx?
-
require 'monitor'
-
require 'concurrent/thread_safe/util/data_structures'
-
-
class RbxHash < ::Hash
-
end
-
ThreadSafe::Util.make_synchronized_on_rbx RbxHash
-
RbxHash
-
-
when Concurrent.on_truffleruby?
-
require 'concurrent/thread_safe/util/data_structures'
-
-
class TruffleRubyHash < ::Hash
-
end
-
-
ThreadSafe::Util.make_synchronized_on_truffleruby TruffleRubyHash
-
TruffleRubyHash
-
-
else
-
warn 'Possibly unsupported Ruby implementation'
-
::Hash
-
end
-
1
private_constant :HashImplementation
-
-
# @!macro concurrent_hash
-
1
class Hash < HashImplementation
-
end
-
-
end
-
1
require 'concurrent/synchronization/abstract_struct'
-
1
require 'concurrent/synchronization'
-
-
1
module Concurrent
-
-
# A thread-safe, immutable variation of Ruby's standard `Struct`.
-
#
-
# @see http://ruby-doc.org/core-2.2.0/Struct.html Ruby standard library `Struct`
-
1
module ImmutableStruct
-
1
include Synchronization::AbstractStruct
-
-
1
def self.included(base)
-
base.safe_initialization!
-
end
-
-
# @!macro struct_values
-
1
def values
-
ns_values
-
end
-
-
1
alias_method :to_a, :values
-
-
# @!macro struct_values_at
-
1
def values_at(*indexes)
-
ns_values_at(indexes)
-
end
-
-
# @!macro struct_inspect
-
1
def inspect
-
ns_inspect
-
end
-
-
1
alias_method :to_s, :inspect
-
-
# @!macro struct_merge
-
1
def merge(other, &block)
-
ns_merge(other, &block)
-
end
-
-
# @!macro struct_to_h
-
1
def to_h
-
ns_to_h
-
end
-
-
# @!macro struct_get
-
1
def [](member)
-
ns_get(member)
-
end
-
-
# @!macro struct_equality
-
1
def ==(other)
-
ns_equality(other)
-
end
-
-
# @!macro struct_each
-
1
def each(&block)
-
return enum_for(:each) unless block_given?
-
ns_each(&block)
-
end
-
-
# @!macro struct_each_pair
-
1
def each_pair(&block)
-
return enum_for(:each_pair) unless block_given?
-
ns_each_pair(&block)
-
end
-
-
# @!macro struct_select
-
1
def select(&block)
-
return enum_for(:select) unless block_given?
-
ns_select(&block)
-
end
-
-
1
private
-
-
# @!visibility private
-
1
def initialize_copy(original)
-
super(original)
-
ns_initialize_copy
-
end
-
-
# @!macro struct_new
-
1
def self.new(*args, &block)
-
clazz_name = nil
-
if args.length == 0
-
raise ArgumentError.new('wrong number of arguments (0 for 1+)')
-
elsif args.length > 0 && args.first.is_a?(String)
-
clazz_name = args.shift
-
end
-
FACTORY.define_struct(clazz_name, args, &block)
-
end
-
-
1
FACTORY = Class.new(Synchronization::LockableObject) do
-
1
def define_struct(name, members, &block)
-
synchronize do
-
Synchronization::AbstractStruct.define_struct_class(ImmutableStruct, Synchronization::Object, name, members, &block)
-
end
-
end
-
end.new
-
1
private_constant :FACTORY
-
end
-
end
-
1
require 'concurrent/constants'
-
1
require 'concurrent/errors'
-
1
require 'concurrent/collection/copy_on_write_observer_set'
-
1
require 'concurrent/concern/obligation'
-
1
require 'concurrent/concern/observable'
-
1
require 'concurrent/synchronization'
-
-
1
module Concurrent
-
-
# An `IVar` is like a future that you can assign. As a future is a value that
-
# is being computed that you can wait on, an `IVar` is a value that is waiting
-
# to be assigned, that you can wait on. `IVars` are single assignment and
-
# deterministic.
-
#
-
# Then, express futures as an asynchronous computation that assigns an `IVar`.
-
# The `IVar` becomes the primitive on which [futures](Future) and
-
# [dataflow](Dataflow) are built.
-
#
-
# An `IVar` is a single-element container that is normally created empty, and
-
# can only be set once. The I in `IVar` stands for immutable. Reading an
-
# `IVar` normally blocks until it is set. It is safe to set and read an `IVar`
-
# from different threads.
-
#
-
# If you want to have some parallel task set the value in an `IVar`, you want
-
# a `Future`. If you want to create a graph of parallel tasks all executed
-
# when the values they depend on are ready you want `dataflow`. `IVar` is
-
# generally a low-level primitive.
-
#
-
# ## Examples
-
#
-
# Create, set and get an `IVar`
-
#
-
# ```ruby
-
# ivar = Concurrent::IVar.new
-
# ivar.set 14
-
# ivar.value #=> 14
-
# ivar.set 2 # would now be an error
-
# ```
-
#
-
# ## See Also
-
#
-
# 1. For the theory: Arvind, R. Nikhil, and K. Pingali.
-
# [I-Structures: Data structures for parallel computing](http://dl.acm.org/citation.cfm?id=69562).
-
# In Proceedings of Workshop on Graph Reduction, 1986.
-
# 2. For recent application:
-
# [DataDrivenFuture in Habanero Java from Rice](http://www.cs.rice.edu/~vs3/hjlib/doc/edu/rice/hj/api/HjDataDrivenFuture.html).
-
1
class IVar < Synchronization::LockableObject
-
1
include Concern::Obligation
-
1
include Concern::Observable
-
-
# Create a new `IVar` in the `:pending` state with the (optional) initial value.
-
#
-
# @param [Object] value the initial value
-
# @param [Hash] opts the options to create a message with
-
# @option opts [String] :dup_on_deref (false) call `#dup` before returning
-
# the data
-
# @option opts [String] :freeze_on_deref (false) call `#freeze` before
-
# returning the data
-
# @option opts [String] :copy_on_deref (nil) call the given `Proc` passing
-
# the internal value and returning the value returned from the proc
-
1
def initialize(value = NULL, opts = {}, &block)
-
if value != NULL && block_given?
-
raise ArgumentError.new('provide only a value or a block')
-
end
-
super(&nil)
-
synchronize { ns_initialize(value, opts, &block) }
-
end
-
-
# Add an observer on this object that will receive notification on update.
-
#
-
# Upon completion the `IVar` will notify all observers in a thread-safe way.
-
# The `func` method of the observer will be called with three arguments: the
-
# `Time` at which the `Future` completed the asynchronous operation, the
-
# final `value` (or `nil` on rejection), and the final `reason` (or `nil` on
-
# fulfillment).
-
#
-
# @param [Object] observer the object that will be notified of changes
-
# @param [Symbol] func symbol naming the method to call when this
-
# `Observable` has changes`
-
1
def add_observer(observer = nil, func = :update, &block)
-
raise ArgumentError.new('cannot provide both an observer and a block') if observer && block
-
direct_notification = false
-
-
if block
-
observer = block
-
func = :call
-
end
-
-
synchronize do
-
if event.set?
-
direct_notification = true
-
else
-
observers.add_observer(observer, func)
-
end
-
end
-
-
observer.send(func, Time.now, self.value, reason) if direct_notification
-
observer
-
end
-
-
# @!macro ivar_set_method
-
# Set the `IVar` to a value and wake or notify all threads waiting on it.
-
#
-
# @!macro ivar_set_parameters_and_exceptions
-
# @param [Object] value the value to store in the `IVar`
-
# @yield A block operation to use for setting the value
-
# @raise [ArgumentError] if both a value and a block are given
-
# @raise [Concurrent::MultipleAssignmentError] if the `IVar` has already
-
# been set or otherwise completed
-
#
-
# @return [IVar] self
-
1
def set(value = NULL)
-
check_for_block_or_value!(block_given?, value)
-
raise MultipleAssignmentError unless compare_and_set_state(:processing, :pending)
-
-
begin
-
value = yield if block_given?
-
complete_without_notification(true, value, nil)
-
rescue => ex
-
complete_without_notification(false, nil, ex)
-
end
-
-
notify_observers(self.value, reason)
-
self
-
end
-
-
# @!macro ivar_fail_method
-
# Set the `IVar` to failed due to some error and wake or notify all threads waiting on it.
-
#
-
# @param [Object] reason for the failure
-
# @raise [Concurrent::MultipleAssignmentError] if the `IVar` has already
-
# been set or otherwise completed
-
# @return [IVar] self
-
1
def fail(reason = StandardError.new)
-
complete(false, nil, reason)
-
end
-
-
# Attempt to set the `IVar` with the given value or block. Return a
-
# boolean indicating the success or failure of the set operation.
-
#
-
# @!macro ivar_set_parameters_and_exceptions
-
#
-
# @return [Boolean] true if the value was set else false
-
1
def try_set(value = NULL, &block)
-
set(value, &block)
-
true
-
rescue MultipleAssignmentError
-
false
-
end
-
-
1
protected
-
-
# @!visibility private
-
1
def ns_initialize(value, opts)
-
value = yield if block_given?
-
init_obligation
-
self.observers = Collection::CopyOnWriteObserverSet.new
-
set_deref_options(opts)
-
-
@state = :pending
-
if value != NULL
-
ns_complete_without_notification(true, value, nil)
-
end
-
end
-
-
# @!visibility private
-
1
def safe_execute(task, args = [])
-
if compare_and_set_state(:processing, :pending)
-
success, val, reason = SafeTaskExecutor.new(task, rescue_exception: true).execute(*@args)
-
complete(success, val, reason)
-
yield(success, val, reason) if block_given?
-
end
-
end
-
-
# @!visibility private
-
1
def complete(success, value, reason)
-
complete_without_notification(success, value, reason)
-
notify_observers(self.value, reason)
-
self
-
end
-
-
# @!visibility private
-
1
def complete_without_notification(success, value, reason)
-
synchronize { ns_complete_without_notification(success, value, reason) }
-
self
-
end
-
-
# @!visibility private
-
1
def notify_observers(value, reason)
-
observers.notify_and_delete_observers{ [Time.now, value, reason] }
-
end
-
-
# @!visibility private
-
1
def ns_complete_without_notification(success, value, reason)
-
raise MultipleAssignmentError if [:fulfilled, :rejected].include? @state
-
set_state(success, value, reason)
-
event.set
-
end
-
-
# @!visibility private
-
1
def check_for_block_or_value!(block_given, value) # :nodoc:
-
if (block_given && value != NULL) || (! block_given && value == NULL)
-
raise ArgumentError.new('must set with either a value or a block')
-
end
-
end
-
end
-
end
-
1
require 'thread'
-
1
require 'concurrent/constants'
-
1
require 'concurrent/synchronization'
-
1
require 'concurrent/utility/engine'
-
-
1
module Concurrent
-
# @!visibility private
-
1
module Collection
-
-
# @!visibility private
-
MapImplementation = case
-
1
when Concurrent.on_jruby?
-
# noinspection RubyResolve
-
JRubyMapBackend
-
when Concurrent.on_cruby?
-
1
require 'concurrent/collection/map/mri_map_backend'
-
1
MriMapBackend
-
when Concurrent.on_rbx? || Concurrent.on_truffleruby?
-
require 'concurrent/collection/map/atomic_reference_map_backend'
-
AtomicReferenceMapBackend
-
else
-
warn 'Concurrent::Map: unsupported Ruby engine, using a fully synchronized Concurrent::Map implementation'
-
require 'concurrent/collection/map/synchronized_map_backend'
-
SynchronizedMapBackend
-
end
-
end
-
-
# `Concurrent::Map` is a hash-like object and should have much better performance
-
# characteristics, especially under high concurrency, than `Concurrent::Hash`.
-
# However, `Concurrent::Map `is not strictly semantically equivalent to a ruby `Hash`
-
# -- for instance, it does not necessarily retain ordering by insertion time as `Hash`
-
# does. For most uses it should do fine though, and we recommend you consider
-
# `Concurrent::Map` instead of `Concurrent::Hash` for your concurrency-safe hash needs.
-
1
class Map < Collection::MapImplementation
-
-
# @!macro map.atomic_method
-
# This method is atomic.
-
-
# @!macro map.atomic_method_with_block
-
# This method is atomic.
-
# @note Atomic methods taking a block do not allow the `self` instance
-
# to be used within the block. Doing so will cause a deadlock.
-
-
# @!method compute_if_absent(key)
-
# Compute and store new value for key if the key is absent.
-
# @param [Object] key
-
# @yield new value
-
# @yieldreturn [Object] new value
-
# @return [Object] new value or current value
-
# @!macro map.atomic_method_with_block
-
-
# @!method compute_if_present(key)
-
# Compute and store new value for key if the key is present.
-
# @param [Object] key
-
# @yield new value
-
# @yieldparam old_value [Object]
-
# @yieldreturn [Object, nil] new value, when nil the key is removed
-
# @return [Object, nil] new value or nil
-
# @!macro map.atomic_method_with_block
-
-
# @!method compute(key)
-
# Compute and store new value for key.
-
# @param [Object] key
-
# @yield compute new value from old one
-
# @yieldparam old_value [Object, nil] old_value, or nil when key is absent
-
# @yieldreturn [Object, nil] new value, when nil the key is removed
-
# @return [Object, nil] new value or nil
-
# @!macro map.atomic_method_with_block
-
-
# @!method merge_pair(key, value)
-
# If the key is absent, the value is stored, otherwise new value is
-
# computed with a block.
-
# @param [Object] key
-
# @param [Object] value
-
# @yield compute new value from old one
-
# @yieldparam old_value [Object] old value
-
# @yieldreturn [Object, nil] new value, when nil the key is removed
-
# @return [Object, nil] new value or nil
-
# @!macro map.atomic_method_with_block
-
-
# @!method replace_pair(key, old_value, new_value)
-
# Replaces old_value with new_value if key exists and current value
-
# matches old_value
-
# @param [Object] key
-
# @param [Object] old_value
-
# @param [Object] new_value
-
# @return [true, false] true if replaced
-
# @!macro map.atomic_method
-
-
# @!method replace_if_exists(key, new_value)
-
# Replaces current value with new_value if key exists
-
# @param [Object] key
-
# @param [Object] new_value
-
# @return [Object, nil] old value or nil
-
# @!macro map.atomic_method
-
-
# @!method get_and_set(key, value)
-
# Get the current value under key and set new value.
-
# @param [Object] key
-
# @param [Object] value
-
# @return [Object, nil] old value or nil when the key was absent
-
# @!macro map.atomic_method
-
-
# @!method delete(key)
-
# Delete key and its value.
-
# @param [Object] key
-
# @return [Object, nil] old value or nil when the key was absent
-
# @!macro map.atomic_method
-
-
# @!method delete_pair(key, value)
-
# Delete pair and its value if current value equals the provided value.
-
# @param [Object] key
-
# @param [Object] value
-
# @return [true, false] true if deleted
-
# @!macro map.atomic_method
-
-
-
1
def initialize(options = nil, &block)
-
60
if options.kind_of?(::Hash)
-
validate_options_hash!(options)
-
else
-
60
options = nil
-
end
-
-
60
super(options)
-
60
@default_proc = block
-
end
-
-
# Get a value with key
-
# @param [Object] key
-
# @return [Object] the value
-
1
def [](key)
-
if value = super # non-falsy value is an existing mapping, return it right away
-
value
-
# re-check is done with get_or_default(key, NULL) instead of a simple !key?(key) in order to avoid a race condition, whereby by the time the current thread gets to the key?(key) call
-
# a key => value mapping might have already been created by a different thread (key?(key) would then return true, this elsif branch wouldn't be taken and an incorrent +nil+ value
-
# would be returned)
-
# note: nil == value check is not technically necessary
-
elsif @default_proc && nil == value && NULL == (value = get_or_default(key, NULL))
-
@default_proc.call(self, key)
-
else
-
value
-
end
-
end
-
-
1
alias_method :get, :[]
-
# TODO (pitr-ch 30-Oct-2018): doc
-
1
alias_method :put, :[]=
-
-
# Get a value with key, or default_value when key is absent,
-
# or fail when no default_value is given.
-
# @param [Object] key
-
# @param [Object] default_value
-
# @yield default value for a key
-
# @yieldparam key [Object]
-
# @yieldreturn [Object] default value
-
# @return [Object] the value or default value
-
# @raise [KeyError] when key is missing and no default_value is provided
-
# @!macro map_method_not_atomic
-
# @note The "fetch-then-act" methods of `Map` are not atomic. `Map` is intended
-
# to be use as a concurrency primitive with strong happens-before
-
# guarantees. It is not intended to be used as a high-level abstraction
-
# supporting complex operations. All read and write operations are
-
# thread safe, but no guarantees are made regarding race conditions
-
# between the fetch operation and yielding to the block. Additionally,
-
# this method does not support recursion. This is due to internal
-
# constraints that are very unlikely to change in the near future.
-
1
def fetch(key, default_value = NULL)
-
if NULL != (value = get_or_default(key, NULL))
-
value
-
elsif block_given?
-
yield key
-
elsif NULL != default_value
-
default_value
-
else
-
raise_fetch_no_key
-
end
-
end
-
-
# Fetch value with key, or store default value when key is absent,
-
# or fail when no default_value is given. This is a two step operation,
-
# therefore not atomic. The store can overwrite other concurrently
-
# stored value.
-
# @param [Object] key
-
# @param [Object] default_value
-
# @yield default value for a key
-
# @yieldparam key [Object]
-
# @yieldreturn [Object] default value
-
# @return [Object] the value or default value
-
# @!macro map.atomic_method_with_block
-
1
def fetch_or_store(key, default_value = NULL)
-
fetch(key) do
-
put(key, block_given? ? yield(key) : (NULL == default_value ? raise_fetch_no_key : default_value))
-
end
-
end
-
-
# Insert value into map with key if key is absent in one atomic step.
-
# @param [Object] key
-
# @param [Object] value
-
# @return [Object, nil] the previous value when key was present or nil when there was no key
-
def put_if_absent(key, value)
-
computed = false
-
result = compute_if_absent(key) do
-
computed = true
-
value
-
end
-
computed ? nil : result
-
1
end unless method_defined?(:put_if_absent)
-
-
# Is the value stored in the map. Iterates over all values.
-
# @param [Object] value
-
# @return [true, false]
-
1
def value?(value)
-
each_value do |v|
-
return true if value.equal?(v)
-
end
-
false
-
end
-
-
# All keys
-
# @return [::Array<Object>] keys
-
def keys
-
arr = []
-
each_pair { |k, v| arr << k }
-
arr
-
1
end unless method_defined?(:keys)
-
-
# All values
-
# @return [::Array<Object>] values
-
def values
-
arr = []
-
each_pair { |k, v| arr << v }
-
arr
-
1
end unless method_defined?(:values)
-
-
# Iterates over each key.
-
# @yield for each key in the map
-
# @yieldparam key [Object]
-
# @return [self]
-
# @!macro map.atomic_method_with_block
-
def each_key
-
each_pair { |k, v| yield k }
-
1
end unless method_defined?(:each_key)
-
-
# Iterates over each value.
-
# @yield for each value in the map
-
# @yieldparam value [Object]
-
# @return [self]
-
# @!macro map.atomic_method_with_block
-
def each_value
-
each_pair { |k, v| yield v }
-
1
end unless method_defined?(:each_value)
-
-
# Iterates over each key value pair.
-
# @yield for each key value pair in the map
-
# @yieldparam key [Object]
-
# @yieldparam value [Object]
-
# @return [self]
-
# @!macro map.atomic_method_with_block
-
1
def each_pair
-
return enum_for :each_pair unless block_given?
-
super
-
end
-
-
1
alias_method :each, :each_pair unless method_defined?(:each)
-
-
# Find key of a value.
-
# @param [Object] value
-
# @return [Object, nil] key or nil when not found
-
def key(value)
-
each_pair { |k, v| return k if v == value }
-
nil
-
1
end unless method_defined?(:key)
-
1
alias_method :index, :key if RUBY_VERSION < '1.9'
-
-
# Is map empty?
-
# @return [true, false]
-
def empty?
-
each_pair { |k, v| return false }
-
true
-
1
end unless method_defined?(:empty?)
-
-
# The size of map.
-
# @return [Integer] size
-
def size
-
count = 0
-
each_pair { |k, v| count += 1 }
-
count
-
1
end unless method_defined?(:size)
-
-
# @!visibility private
-
1
def marshal_dump
-
raise TypeError, "can't dump hash with default proc" if @default_proc
-
h = {}
-
each_pair { |k, v| h[k] = v }
-
h
-
end
-
-
# @!visibility private
-
1
def marshal_load(hash)
-
initialize
-
populate_from(hash)
-
end
-
-
1
undef :freeze
-
-
# @!visibility private
-
1
def inspect
-
format '%s entries=%d default_proc=%s>', to_s[0..-2], size.to_s, @default_proc.inspect
-
end
-
-
1
private
-
-
1
def raise_fetch_no_key
-
raise KeyError, 'key not found'
-
end
-
-
1
def initialize_copy(other)
-
super
-
populate_from(other)
-
end
-
-
1
def populate_from(hash)
-
hash.each_pair { |k, v| self[k] = v }
-
self
-
end
-
-
1
def validate_options_hash!(options)
-
if (initial_capacity = options[:initial_capacity]) && (!initial_capacity.kind_of?(Integer) || initial_capacity < 0)
-
raise ArgumentError, ":initial_capacity must be a positive Integer"
-
end
-
if (load_factor = options[:load_factor]) && (!load_factor.kind_of?(Numeric) || load_factor <= 0 || load_factor > 1)
-
raise ArgumentError, ":load_factor must be a number between 0 and 1"
-
end
-
end
-
end
-
end
-
1
require 'concurrent/synchronization'
-
-
1
module Concurrent
-
-
# A `Maybe` encapsulates an optional value. A `Maybe` either contains a value
-
# of (represented as `Just`), or it is empty (represented as `Nothing`). Using
-
# `Maybe` is a good way to deal with errors or exceptional cases without
-
# resorting to drastic measures such as exceptions.
-
#
-
# `Maybe` is a replacement for the use of `nil` with better type checking.
-
#
-
# For compatibility with {Concurrent::Concern::Obligation} the predicate and
-
# accessor methods are aliased as `fulfilled?`, `rejected?`, `value`, and
-
# `reason`.
-
#
-
# ## Motivation
-
#
-
# A common pattern in languages with pattern matching, such as Erlang and
-
# Haskell, is to return *either* a value *or* an error from a function
-
# Consider this Erlang code:
-
#
-
# ```erlang
-
# case file:consult("data.dat") of
-
# {ok, Terms} -> do_something_useful(Terms);
-
# {error, Reason} -> lager:error(Reason)
-
# end.
-
# ```
-
#
-
# In this example the standard library function `file:consult` returns a
-
# [tuple](http://erlang.org/doc/reference_manual/data_types.html#id69044)
-
# with two elements: an [atom](http://erlang.org/doc/reference_manual/data_types.html#id64134)
-
# (similar to a ruby symbol) and a variable containing ancillary data. On
-
# success it returns the atom `ok` and the data from the file. On failure it
-
# returns `error` and a string with an explanation of the problem. With this
-
# pattern there is no ambiguity regarding success or failure. If the file is
-
# empty the return value cannot be misinterpreted as an error. And when an
-
# error occurs the return value provides useful information.
-
#
-
# In Ruby we tend to return `nil` when an error occurs or else we raise an
-
# exception. Both of these idioms are problematic. Returning `nil` is
-
# ambiguous because `nil` may also be a valid value. It also lacks
-
# information pertaining to the nature of the error. Raising an exception
-
# is both expensive and usurps the normal flow of control. All of these
-
# problems can be solved with the use of a `Maybe`.
-
#
-
# A `Maybe` is unambiguous with regard to whether or not it contains a value.
-
# When `Just` it contains a value, when `Nothing` it does not. When `Just`
-
# the value it contains may be `nil`, which is perfectly valid. When
-
# `Nothing` the reason for the lack of a value is contained as well. The
-
# previous Erlang example can be duplicated in Ruby in a principled way by
-
# having functions return `Maybe` objects:
-
#
-
# ```ruby
-
# result = MyFileUtils.consult("data.dat") # returns a Maybe
-
# if result.just?
-
# do_something_useful(result.value) # or result.just
-
# else
-
# logger.error(result.reason) # or result.nothing
-
# end
-
# ```
-
#
-
# @example Returning a Maybe from a Function
-
# module MyFileUtils
-
# def self.consult(path)
-
# file = File.open(path, 'r')
-
# Concurrent::Maybe.just(file.read)
-
# rescue => ex
-
# return Concurrent::Maybe.nothing(ex)
-
# ensure
-
# file.close if file
-
# end
-
# end
-
#
-
# maybe = MyFileUtils.consult('bogus.file')
-
# maybe.just? #=> false
-
# maybe.nothing? #=> true
-
# maybe.reason #=> #<Errno::ENOENT: No such file or directory @ rb_sysopen - bogus.file>
-
#
-
# maybe = MyFileUtils.consult('README.md')
-
# maybe.just? #=> true
-
# maybe.nothing? #=> false
-
# maybe.value #=> "# Concurrent Ruby\n[![Gem Version..."
-
#
-
# @example Using Maybe with a Block
-
# result = Concurrent::Maybe.from do
-
# Client.find(10) # Client is an ActiveRecord model
-
# end
-
#
-
# # -- if the record was found
-
# result.just? #=> true
-
# result.value #=> #<Client id: 10, first_name: "Ryan">
-
#
-
# # -- if the record was not found
-
# result.just? #=> false
-
# result.reason #=> ActiveRecord::RecordNotFound
-
#
-
# @example Using Maybe with the Null Object Pattern
-
# # In a Rails controller...
-
# result = ClientService.new(10).find # returns a Maybe
-
# render json: result.or(NullClient.new)
-
#
-
# @see https://hackage.haskell.org/package/base-4.2.0.1/docs/Data-Maybe.html Haskell Data.Maybe
-
# @see https://github.com/purescript/purescript-maybe/blob/master/docs/Data.Maybe.md PureScript Data.Maybe
-
1
class Maybe < Synchronization::Object
-
1
include Comparable
-
1
safe_initialization!
-
-
# Indicates that the given attribute has not been set.
-
# When `Just` the {#nothing} getter will return `NONE`.
-
# When `Nothing` the {#just} getter will return `NONE`.
-
1
NONE = ::Object.new.freeze
-
-
# The value of a `Maybe` when `Just`. Will be `NONE` when `Nothing`.
-
1
attr_reader :just
-
-
# The reason for the `Maybe` when `Nothing`. Will be `NONE` when `Just`.
-
1
attr_reader :nothing
-
-
1
private_class_method :new
-
-
# Create a new `Maybe` using the given block.
-
#
-
# Runs the given block passing all function arguments to the block as block
-
# arguments. If the block runs to completion without raising an exception
-
# a new `Just` is created with the value set to the return value of the
-
# block. If the block raises an exception a new `Nothing` is created with
-
# the reason being set to the raised exception.
-
#
-
# @param [Array<Object>] args Zero or more arguments to pass to the block.
-
# @yield The block from which to create a new `Maybe`.
-
# @yieldparam [Array<Object>] args Zero or more block arguments passed as
-
# arguments to the function.
-
#
-
# @return [Maybe] The newly created object.
-
#
-
# @raise [ArgumentError] when no block given.
-
1
def self.from(*args)
-
raise ArgumentError.new('no block given') unless block_given?
-
begin
-
value = yield(*args)
-
return new(value, NONE)
-
rescue => ex
-
return new(NONE, ex)
-
end
-
end
-
-
# Create a new `Just` with the given value.
-
#
-
# @param [Object] value The value to set for the new `Maybe` object.
-
#
-
# @return [Maybe] The newly created object.
-
1
def self.just(value)
-
return new(value, NONE)
-
end
-
-
# Create a new `Nothing` with the given (optional) reason.
-
#
-
# @param [Exception] error The reason to set for the new `Maybe` object.
-
# When given a string a new `StandardError` will be created with the
-
# argument as the message. When no argument is given a new
-
# `StandardError` with an empty message will be created.
-
#
-
# @return [Maybe] The newly created object.
-
1
def self.nothing(error = '')
-
if error.is_a?(Exception)
-
nothing = error
-
else
-
nothing = StandardError.new(error.to_s)
-
end
-
return new(NONE, nothing)
-
end
-
-
# Is this `Maybe` a `Just` (successfully fulfilled with a value)?
-
#
-
# @return [Boolean] True if `Just` or false if `Nothing`.
-
1
def just?
-
! nothing?
-
end
-
1
alias :fulfilled? :just?
-
-
# Is this `Maybe` a `nothing` (rejected with an exception upon fulfillment)?
-
#
-
# @return [Boolean] True if `Nothing` or false if `Just`.
-
1
def nothing?
-
@nothing != NONE
-
end
-
1
alias :rejected? :nothing?
-
-
1
alias :value :just
-
-
1
alias :reason :nothing
-
-
# Comparison operator.
-
#
-
# @return [Integer] 0 if self and other are both `Nothing`;
-
# -1 if self is `Nothing` and other is `Just`;
-
# 1 if self is `Just` and other is nothing;
-
# `self.just <=> other.just` if both self and other are `Just`.
-
1
def <=>(other)
-
if nothing?
-
other.nothing? ? 0 : -1
-
else
-
other.nothing? ? 1 : just <=> other.just
-
end
-
end
-
-
# Return either the value of self or the given default value.
-
#
-
# @return [Object] The value of self when `Just`; else the given default.
-
1
def or(other)
-
just? ? just : other
-
end
-
-
1
private
-
-
# Create a new `Maybe` with the given attributes.
-
#
-
# @param [Object] just The value when `Just` else `NONE`.
-
# @param [Exception, Object] nothing The exception when `Nothing` else `NONE`.
-
#
-
# @return [Maybe] The new `Maybe`.
-
#
-
# @!visibility private
-
1
def initialize(just, nothing)
-
@just = just
-
@nothing = nothing
-
end
-
end
-
end
-
1
require 'concurrent/synchronization/abstract_struct'
-
1
require 'concurrent/synchronization'
-
-
1
module Concurrent
-
-
# An thread-safe variation of Ruby's standard `Struct`. Values can be set at
-
# construction or safely changed at any time during the object's lifecycle.
-
#
-
# @see http://ruby-doc.org/core-2.2.0/Struct.html Ruby standard library `Struct`
-
1
module MutableStruct
-
1
include Synchronization::AbstractStruct
-
-
# @!macro struct_new
-
#
-
# Factory for creating new struct classes.
-
#
-
# ```
-
# new([class_name] [, member_name]+>) -> StructClass click to toggle source
-
# new([class_name] [, member_name]+>) {|StructClass| block } -> StructClass
-
# new(value, ...) -> obj
-
# StructClass[value, ...] -> obj
-
# ```
-
#
-
# The first two forms are used to create a new struct subclass `class_name`
-
# that can contain a value for each member_name . This subclass can be
-
# used to create instances of the structure like any other Class .
-
#
-
# If the `class_name` is omitted an anonymous struct class will be created.
-
# Otherwise, the name of this struct will appear as a constant in the struct class,
-
# so it must be unique for all structs under this base class and must start with a
-
# capital letter. Assigning a struct class to a constant also gives the class
-
# the name of the constant.
-
#
-
# If a block is given it will be evaluated in the context of `StructClass`, passing
-
# the created class as a parameter. This is the recommended way to customize a struct.
-
# Subclassing an anonymous struct creates an extra anonymous class that will never be used.
-
#
-
# The last two forms create a new instance of a struct subclass. The number of value
-
# parameters must be less than or equal to the number of attributes defined for the
-
# struct. Unset parameters default to nil. Passing more parameters than number of attributes
-
# will raise an `ArgumentError`.
-
#
-
# @see http://ruby-doc.org/core-2.2.0/Struct.html#method-c-new Ruby standard library `Struct#new`
-
-
# @!macro struct_values
-
#
-
# Returns the values for this struct as an Array.
-
#
-
# @return [Array] the values for this struct
-
#
-
1
def values
-
synchronize { ns_values }
-
end
-
1
alias_method :to_a, :values
-
-
# @!macro struct_values_at
-
#
-
# Returns the struct member values for each selector as an Array.
-
#
-
# A selector may be either an Integer offset or a Range of offsets (as in `Array#values_at`).
-
#
-
# @param [Fixnum, Range] indexes the index(es) from which to obatin the values (in order)
-
1
def values_at(*indexes)
-
synchronize { ns_values_at(indexes) }
-
end
-
-
# @!macro struct_inspect
-
#
-
# Describe the contents of this struct in a string.
-
#
-
# @return [String] the contents of this struct in a string
-
1
def inspect
-
synchronize { ns_inspect }
-
end
-
1
alias_method :to_s, :inspect
-
-
# @!macro struct_merge
-
#
-
# Returns a new struct containing the contents of `other` and the contents
-
# of `self`. If no block is specified, the value for entries with duplicate
-
# keys will be that of `other`. Otherwise the value for each duplicate key
-
# is determined by calling the block with the key, its value in `self` and
-
# its value in `other`.
-
#
-
# @param [Hash] other the hash from which to set the new values
-
# @yield an options block for resolving duplicate keys
-
# @yieldparam [String, Symbol] member the name of the member which is duplicated
-
# @yieldparam [Object] selfvalue the value of the member in `self`
-
# @yieldparam [Object] othervalue the value of the member in `other`
-
#
-
# @return [Synchronization::AbstractStruct] a new struct with the new values
-
#
-
# @raise [ArgumentError] of given a member that is not defined in the struct
-
1
def merge(other, &block)
-
synchronize { ns_merge(other, &block) }
-
end
-
-
# @!macro struct_to_h
-
#
-
# Returns a hash containing the names and values for the struct’s members.
-
#
-
# @return [Hash] the names and values for the struct’s members
-
1
def to_h
-
synchronize { ns_to_h }
-
end
-
-
# @!macro struct_get
-
#
-
# Attribute Reference
-
#
-
# @param [Symbol, String, Integer] member the string or symbol name of the member
-
# for which to obtain the value or the member's index
-
#
-
# @return [Object] the value of the given struct member or the member at the given index.
-
#
-
# @raise [NameError] if the member does not exist
-
# @raise [IndexError] if the index is out of range.
-
1
def [](member)
-
synchronize { ns_get(member) }
-
end
-
-
# @!macro struct_equality
-
#
-
# Equality
-
#
-
# @return [Boolean] true if other has the same struct subclass and has
-
# equal member values (according to `Object#==`)
-
1
def ==(other)
-
synchronize { ns_equality(other) }
-
end
-
-
# @!macro struct_each
-
#
-
# Yields the value of each struct member in order. If no block is given
-
# an enumerator is returned.
-
#
-
# @yield the operation to be performed on each struct member
-
# @yieldparam [Object] value each struct value (in order)
-
1
def each(&block)
-
return enum_for(:each) unless block_given?
-
synchronize { ns_each(&block) }
-
end
-
-
# @!macro struct_each_pair
-
#
-
# Yields the name and value of each struct member in order. If no block is
-
# given an enumerator is returned.
-
#
-
# @yield the operation to be performed on each struct member/value pair
-
# @yieldparam [Object] member each struct member (in order)
-
# @yieldparam [Object] value each struct value (in order)
-
1
def each_pair(&block)
-
return enum_for(:each_pair) unless block_given?
-
synchronize { ns_each_pair(&block) }
-
end
-
-
# @!macro struct_select
-
#
-
# Yields each member value from the struct to the block and returns an Array
-
# containing the member values from the struct for which the given block
-
# returns a true value (equivalent to `Enumerable#select`).
-
#
-
# @yield the operation to be performed on each struct member
-
# @yieldparam [Object] value each struct value (in order)
-
#
-
# @return [Array] an array containing each value for which the block returns true
-
1
def select(&block)
-
return enum_for(:select) unless block_given?
-
synchronize { ns_select(&block) }
-
end
-
-
# @!macro struct_set
-
#
-
# Attribute Assignment
-
#
-
# Sets the value of the given struct member or the member at the given index.
-
#
-
# @param [Symbol, String, Integer] member the string or symbol name of the member
-
# for which to obtain the value or the member's index
-
#
-
# @return [Object] the value of the given struct member or the member at the given index.
-
#
-
# @raise [NameError] if the name does not exist
-
# @raise [IndexError] if the index is out of range.
-
1
def []=(member, value)
-
if member.is_a? Integer
-
length = synchronize { @values.length }
-
if member >= length
-
raise IndexError.new("offset #{member} too large for struct(size:#{length})")
-
end
-
synchronize { @values[member] = value }
-
else
-
send("#{member}=", value)
-
end
-
rescue NoMethodError
-
raise NameError.new("no member '#{member}' in struct")
-
end
-
-
1
private
-
-
# @!visibility private
-
1
def initialize_copy(original)
-
synchronize do
-
super(original)
-
ns_initialize_copy
-
end
-
end
-
-
# @!macro struct_new
-
1
def self.new(*args, &block)
-
clazz_name = nil
-
if args.length == 0
-
raise ArgumentError.new('wrong number of arguments (0 for 1+)')
-
elsif args.length > 0 && args.first.is_a?(String)
-
clazz_name = args.shift
-
end
-
FACTORY.define_struct(clazz_name, args, &block)
-
end
-
-
1
FACTORY = Class.new(Synchronization::LockableObject) do
-
1
def define_struct(name, members, &block)
-
synchronize do
-
clazz = Synchronization::AbstractStruct.define_struct_class(MutableStruct, Synchronization::LockableObject, name, members, &block)
-
members.each_with_index do |member, index|
-
clazz.send :remove_method, member
-
clazz.send(:define_method, member) do
-
synchronize { @values[index] }
-
end
-
clazz.send(:define_method, "#{member}=") do |value|
-
synchronize { @values[index] = value }
-
end
-
end
-
clazz
-
end
-
end
-
end.new
-
1
private_constant :FACTORY
-
end
-
end
-
1
require 'concurrent/concern/dereferenceable'
-
1
require 'concurrent/synchronization'
-
-
1
module Concurrent
-
-
# An `MVar` is a synchronized single element container. They are empty or
-
# contain one item. Taking a value from an empty `MVar` blocks, as does
-
# putting a value into a full one. You can either think of them as blocking
-
# queue of length one, or a special kind of mutable variable.
-
#
-
# On top of the fundamental `#put` and `#take` operations, we also provide a
-
# `#mutate` that is atomic with respect to operations on the same instance.
-
# These operations all support timeouts.
-
#
-
# We also support non-blocking operations `#try_put!` and `#try_take!`, a
-
# `#set!` that ignores existing values, a `#value` that returns the value
-
# without removing it or returns `MVar::EMPTY`, and a `#modify!` that yields
-
# `MVar::EMPTY` if the `MVar` is empty and can be used to set `MVar::EMPTY`.
-
# You shouldn't use these operations in the first instance.
-
#
-
# `MVar` is a [Dereferenceable](Dereferenceable).
-
#
-
# `MVar` is related to M-structures in Id, `MVar` in Haskell and `SyncVar` in Scala.
-
#
-
# Note that unlike the original Haskell paper, our `#take` is blocking. This is how
-
# Haskell and Scala do it today.
-
#
-
# @!macro copy_options
-
#
-
# ## See Also
-
#
-
# 1. P. Barth, R. Nikhil, and Arvind. [M-Structures: Extending a parallel, non- strict, functional language with state](http://dl.acm.org/citation.cfm?id=652538). In Proceedings of the 5th
-
# ACM Conference on Functional Programming Languages and Computer Architecture (FPCA), 1991.
-
#
-
# 2. S. Peyton Jones, A. Gordon, and S. Finne. [Concurrent Haskell](http://dl.acm.org/citation.cfm?id=237794).
-
# In Proceedings of the 23rd Symposium on Principles of Programming Languages
-
# (PoPL), 1996.
-
1
class MVar < Synchronization::Object
-
1
include Concern::Dereferenceable
-
1
safe_initialization!
-
-
# Unique value that represents that an `MVar` was empty
-
1
EMPTY = ::Object.new
-
-
# Unique value that represents that an `MVar` timed out before it was able
-
# to produce a value.
-
1
TIMEOUT = ::Object.new
-
-
# Create a new `MVar`, either empty or with an initial value.
-
#
-
# @param [Hash] opts the options controlling how the future will be processed
-
#
-
# @!macro deref_options
-
1
def initialize(value = EMPTY, opts = {})
-
@value = value
-
@mutex = Mutex.new
-
@empty_condition = ConditionVariable.new
-
@full_condition = ConditionVariable.new
-
set_deref_options(opts)
-
end
-
-
# Remove the value from an `MVar`, leaving it empty, and blocking if there
-
# isn't a value. A timeout can be set to limit the time spent blocked, in
-
# which case it returns `TIMEOUT` if the time is exceeded.
-
# @return [Object] the value that was taken, or `TIMEOUT`
-
1
def take(timeout = nil)
-
@mutex.synchronize do
-
wait_for_full(timeout)
-
-
# If we timed out we'll still be empty
-
if unlocked_full?
-
value = @value
-
@value = EMPTY
-
@empty_condition.signal
-
apply_deref_options(value)
-
else
-
TIMEOUT
-
end
-
end
-
end
-
-
# acquires lock on the from an `MVAR`, yields the value to provided block,
-
# and release lock. A timeout can be set to limit the time spent blocked,
-
# in which case it returns `TIMEOUT` if the time is exceeded.
-
# @return [Object] the value returned by the block, or `TIMEOUT`
-
1
def borrow(timeout = nil)
-
@mutex.synchronize do
-
wait_for_full(timeout)
-
-
# if we timeoud out we'll still be empty
-
if unlocked_full?
-
yield @value
-
else
-
TIMEOUT
-
end
-
end
-
end
-
-
# Put a value into an `MVar`, blocking if there is already a value until
-
# it is empty. A timeout can be set to limit the time spent blocked, in
-
# which case it returns `TIMEOUT` if the time is exceeded.
-
# @return [Object] the value that was put, or `TIMEOUT`
-
1
def put(value, timeout = nil)
-
@mutex.synchronize do
-
wait_for_empty(timeout)
-
-
# If we timed out we won't be empty
-
if unlocked_empty?
-
@value = value
-
@full_condition.signal
-
apply_deref_options(value)
-
else
-
TIMEOUT
-
end
-
end
-
end
-
-
# Atomically `take`, yield the value to a block for transformation, and then
-
# `put` the transformed value. Returns the transformed value. A timeout can
-
# be set to limit the time spent blocked, in which case it returns `TIMEOUT`
-
# if the time is exceeded.
-
# @return [Object] the transformed value, or `TIMEOUT`
-
1
def modify(timeout = nil)
-
raise ArgumentError.new('no block given') unless block_given?
-
-
@mutex.synchronize do
-
wait_for_full(timeout)
-
-
# If we timed out we'll still be empty
-
if unlocked_full?
-
value = @value
-
@value = yield value
-
@full_condition.signal
-
apply_deref_options(value)
-
else
-
TIMEOUT
-
end
-
end
-
end
-
-
# Non-blocking version of `take`, that returns `EMPTY` instead of blocking.
-
1
def try_take!
-
@mutex.synchronize do
-
if unlocked_full?
-
value = @value
-
@value = EMPTY
-
@empty_condition.signal
-
apply_deref_options(value)
-
else
-
EMPTY
-
end
-
end
-
end
-
-
# Non-blocking version of `put`, that returns whether or not it was successful.
-
1
def try_put!(value)
-
@mutex.synchronize do
-
if unlocked_empty?
-
@value = value
-
@full_condition.signal
-
true
-
else
-
false
-
end
-
end
-
end
-
-
# Non-blocking version of `put` that will overwrite an existing value.
-
1
def set!(value)
-
@mutex.synchronize do
-
old_value = @value
-
@value = value
-
@full_condition.signal
-
apply_deref_options(old_value)
-
end
-
end
-
-
# Non-blocking version of `modify` that will yield with `EMPTY` if there is no value yet.
-
1
def modify!
-
raise ArgumentError.new('no block given') unless block_given?
-
-
@mutex.synchronize do
-
value = @value
-
@value = yield value
-
if unlocked_empty?
-
@empty_condition.signal
-
else
-
@full_condition.signal
-
end
-
apply_deref_options(value)
-
end
-
end
-
-
# Returns if the `MVar` is currently empty.
-
1
def empty?
-
@mutex.synchronize { @value == EMPTY }
-
end
-
-
# Returns if the `MVar` currently contains a value.
-
1
def full?
-
!empty?
-
end
-
-
1
protected
-
-
1
def synchronize(&block)
-
@mutex.synchronize(&block)
-
end
-
-
1
private
-
-
1
def unlocked_empty?
-
@value == EMPTY
-
end
-
-
1
def unlocked_full?
-
! unlocked_empty?
-
end
-
-
1
def wait_for_full(timeout)
-
wait_while(@full_condition, timeout) { unlocked_empty? }
-
end
-
-
1
def wait_for_empty(timeout)
-
wait_while(@empty_condition, timeout) { unlocked_full? }
-
end
-
-
1
def wait_while(condition, timeout)
-
if timeout.nil?
-
while yield
-
condition.wait(@mutex)
-
end
-
else
-
stop = Concurrent.monotonic_time + timeout
-
while yield && timeout > 0.0
-
condition.wait(@mutex, timeout)
-
timeout = stop - Concurrent.monotonic_time
-
end
-
end
-
end
-
end
-
end
-
1
require 'concurrent/configuration'
-
-
1
module Concurrent
-
-
# @!visibility private
-
1
module Options
-
-
# Get the requested `Executor` based on the values set in the options hash.
-
#
-
# @param [Hash] opts the options defining the requested executor
-
# @option opts [Executor] :executor when set use the given `Executor` instance.
-
# Three special values are also supported: `:fast` returns the global fast executor,
-
# `:io` returns the global io executor, and `:immediate` returns a new
-
# `ImmediateExecutor` object.
-
#
-
# @return [Executor, nil] the requested thread pool, or nil when no option specified
-
#
-
# @!visibility private
-
1
def self.executor_from_options(opts = {}) # :nodoc:
-
if identifier = opts.fetch(:executor, nil)
-
executor(identifier)
-
else
-
nil
-
end
-
end
-
-
1
def self.executor(executor_identifier)
-
case executor_identifier
-
when :fast
-
Concurrent.global_fast_executor
-
when :io
-
Concurrent.global_io_executor
-
when :immediate
-
Concurrent.global_immediate_executor
-
when Concurrent::ExecutorService
-
executor_identifier
-
else
-
raise ArgumentError, "executor not recognized by '#{executor_identifier}'"
-
end
-
end
-
end
-
end
-
1
require 'thread'
-
1
require 'concurrent/constants'
-
1
require 'concurrent/errors'
-
1
require 'concurrent/ivar'
-
1
require 'concurrent/executor/safe_task_executor'
-
-
1
require 'concurrent/options'
-
-
1
module Concurrent
-
-
1
PromiseExecutionError = Class.new(StandardError)
-
-
# Promises are inspired by the JavaScript [Promises/A](http://wiki.commonjs.org/wiki/Promises/A)
-
# and [Promises/A+](http://promises-aplus.github.io/promises-spec/) specifications.
-
#
-
# > A promise represents the eventual value returned from the single
-
# > completion of an operation.
-
#
-
# Promises are similar to futures and share many of the same behaviours.
-
# Promises are far more robust, however. Promises can be chained in a tree
-
# structure where each promise may have zero or more children. Promises are
-
# chained using the `then` method. The result of a call to `then` is always
-
# another promise. Promises are resolved asynchronously (with respect to the
-
# main thread) but in a strict order: parents are guaranteed to be resolved
-
# before their children, children before their younger siblings. The `then`
-
# method takes two parameters: an optional block to be executed upon parent
-
# resolution and an optional callable to be executed upon parent failure. The
-
# result of each promise is passed to each of its children upon resolution.
-
# When a promise is rejected all its children will be summarily rejected and
-
# will receive the reason.
-
#
-
# Promises have several possible states: *:unscheduled*, *:pending*,
-
# *:processing*, *:rejected*, or *:fulfilled*. These are also aggregated as
-
# `#incomplete?` and `#complete?`. When a Promise is created it is set to
-
# *:unscheduled*. Once the `#execute` method is called the state becomes
-
# *:pending*. Once a job is pulled from the thread pool's queue and is given
-
# to a thread for processing (often immediately upon `#post`) the state
-
# becomes *:processing*. The future will remain in this state until processing
-
# is complete. A future that is in the *:unscheduled*, *:pending*, or
-
# *:processing* is considered `#incomplete?`. A `#complete?` Promise is either
-
# *:rejected*, indicating that an exception was thrown during processing, or
-
# *:fulfilled*, indicating success. If a Promise is *:fulfilled* its `#value`
-
# will be updated to reflect the result of the operation. If *:rejected* the
-
# `reason` will be updated with a reference to the thrown exception. The
-
# predicate methods `#unscheduled?`, `#pending?`, `#rejected?`, and
-
# `#fulfilled?` can be called at any time to obtain the state of the Promise,
-
# as can the `#state` method, which returns a symbol.
-
#
-
# Retrieving the value of a promise is done through the `value` (alias:
-
# `deref`) method. Obtaining the value of a promise is a potentially blocking
-
# operation. When a promise is *rejected* a call to `value` will return `nil`
-
# immediately. When a promise is *fulfilled* a call to `value` will
-
# immediately return the current value. When a promise is *pending* a call to
-
# `value` will block until the promise is either *rejected* or *fulfilled*. A
-
# *timeout* value can be passed to `value` to limit how long the call will
-
# block. If `nil` the call will block indefinitely. If `0` the call will not
-
# block. Any other integer or float value will indicate the maximum number of
-
# seconds to block.
-
#
-
# Promises run on the global thread pool.
-
#
-
# @!macro copy_options
-
#
-
# ### Examples
-
#
-
# Start by requiring promises
-
#
-
# ```ruby
-
# require 'concurrent'
-
# ```
-
#
-
# Then create one
-
#
-
# ```ruby
-
# p = Concurrent::Promise.execute do
-
# # do something
-
# 42
-
# end
-
# ```
-
#
-
# Promises can be chained using the `then` method. The `then` method accepts a
-
# block and an executor, to be executed on fulfillment, and a callable argument to be executed
-
# on rejection. The result of the each promise is passed as the block argument
-
# to chained promises.
-
#
-
# ```ruby
-
# p = Concurrent::Promise.new{10}.then{|x| x * 2}.then{|result| result - 10 }.execute
-
# ```
-
#
-
# And so on, and so on, and so on...
-
#
-
# ```ruby
-
# p = Concurrent::Promise.fulfill(20).
-
# then{|result| result - 10 }.
-
# then{|result| result * 3 }.
-
# then(executor: different_executor){|result| result % 5 }.execute
-
# ```
-
#
-
# The initial state of a newly created Promise depends on the state of its parent:
-
# - if parent is *unscheduled* the child will be *unscheduled*
-
# - if parent is *pending* the child will be *pending*
-
# - if parent is *fulfilled* the child will be *pending*
-
# - if parent is *rejected* the child will be *pending* (but will ultimately be *rejected*)
-
#
-
# Promises are executed asynchronously from the main thread. By the time a
-
# child Promise finishes intialization it may be in a different state than its
-
# parent (by the time a child is created its parent may have completed
-
# execution and changed state). Despite being asynchronous, however, the order
-
# of execution of Promise objects in a chain (or tree) is strictly defined.
-
#
-
# There are multiple ways to create and execute a new `Promise`. Both ways
-
# provide identical behavior:
-
#
-
# ```ruby
-
# # create, operate, then execute
-
# p1 = Concurrent::Promise.new{ "Hello World!" }
-
# p1.state #=> :unscheduled
-
# p1.execute
-
#
-
# # create and immediately execute
-
# p2 = Concurrent::Promise.new{ "Hello World!" }.execute
-
#
-
# # execute during creation
-
# p3 = Concurrent::Promise.execute{ "Hello World!" }
-
# ```
-
#
-
# Once the `execute` method is called a `Promise` becomes `pending`:
-
#
-
# ```ruby
-
# p = Concurrent::Promise.execute{ "Hello, world!" }
-
# p.state #=> :pending
-
# p.pending? #=> true
-
# ```
-
#
-
# Wait a little bit, and the promise will resolve and provide a value:
-
#
-
# ```ruby
-
# p = Concurrent::Promise.execute{ "Hello, world!" }
-
# sleep(0.1)
-
#
-
# p.state #=> :fulfilled
-
# p.fulfilled? #=> true
-
# p.value #=> "Hello, world!"
-
# ```
-
#
-
# If an exception occurs, the promise will be rejected and will provide
-
# a reason for the rejection:
-
#
-
# ```ruby
-
# p = Concurrent::Promise.execute{ raise StandardError.new("Here comes the Boom!") }
-
# sleep(0.1)
-
#
-
# p.state #=> :rejected
-
# p.rejected? #=> true
-
# p.reason #=> "#<StandardError: Here comes the Boom!>"
-
# ```
-
#
-
# #### Rejection
-
#
-
# When a promise is rejected all its children will be rejected and will
-
# receive the rejection `reason` as the rejection callable parameter:
-
#
-
# ```ruby
-
# p = Concurrent::Promise.execute { Thread.pass; raise StandardError }
-
#
-
# c1 = p.then(-> reason { 42 })
-
# c2 = p.then(-> reason { raise 'Boom!' })
-
#
-
# c1.wait.state #=> :fulfilled
-
# c1.value #=> 45
-
# c2.wait.state #=> :rejected
-
# c2.reason #=> #<RuntimeError: Boom!>
-
# ```
-
#
-
# Once a promise is rejected it will continue to accept children that will
-
# receive immediately rejection (they will be executed asynchronously).
-
#
-
# #### Aliases
-
#
-
# The `then` method is the most generic alias: it accepts a block to be
-
# executed upon parent fulfillment and a callable to be executed upon parent
-
# rejection. At least one of them should be passed. The default block is `{
-
# |result| result }` that fulfills the child with the parent value. The
-
# default callable is `{ |reason| raise reason }` that rejects the child with
-
# the parent reason.
-
#
-
# - `on_success { |result| ... }` is the same as `then {|result| ... }`
-
# - `rescue { |reason| ... }` is the same as `then(Proc.new { |reason| ... } )`
-
# - `rescue` is aliased by `catch` and `on_error`
-
1
class Promise < IVar
-
-
# Initialize a new Promise with the provided options.
-
#
-
# @!macro executor_and_deref_options
-
#
-
# @!macro promise_init_options
-
#
-
# @option opts [Promise] :parent the parent `Promise` when building a chain/tree
-
# @option opts [Proc] :on_fulfill fulfillment handler
-
# @option opts [Proc] :on_reject rejection handler
-
# @option opts [object, Array] :args zero or more arguments to be passed
-
# the task block on execution
-
#
-
# @yield The block operation to be performed asynchronously.
-
#
-
# @raise [ArgumentError] if no block is given
-
#
-
# @see http://wiki.commonjs.org/wiki/Promises/A
-
# @see http://promises-aplus.github.io/promises-spec/
-
1
def initialize(opts = {}, &block)
-
opts.delete_if { |k, v| v.nil? }
-
super(NULL, opts.merge(__promise_body_from_block__: block), &nil)
-
end
-
-
# Create a new `Promise` and fulfill it immediately.
-
#
-
# @!macro executor_and_deref_options
-
#
-
# @!macro promise_init_options
-
#
-
# @raise [ArgumentError] if no block is given
-
#
-
# @return [Promise] the newly created `Promise`
-
1
def self.fulfill(value, opts = {})
-
Promise.new(opts).tap { |p| p.send(:synchronized_set_state!, true, value, nil) }
-
end
-
-
# Create a new `Promise` and reject it immediately.
-
#
-
# @!macro executor_and_deref_options
-
#
-
# @!macro promise_init_options
-
#
-
# @raise [ArgumentError] if no block is given
-
#
-
# @return [Promise] the newly created `Promise`
-
1
def self.reject(reason, opts = {})
-
Promise.new(opts).tap { |p| p.send(:synchronized_set_state!, false, nil, reason) }
-
end
-
-
# Execute an `:unscheduled` `Promise`. Immediately sets the state to `:pending` and
-
# passes the block to a new thread/thread pool for eventual execution.
-
# Does nothing if the `Promise` is in any state other than `:unscheduled`.
-
#
-
# @return [Promise] a reference to `self`
-
1
def execute
-
if root?
-
if compare_and_set_state(:pending, :unscheduled)
-
set_pending
-
realize(@promise_body)
-
end
-
else
-
@parent.execute
-
end
-
self
-
end
-
-
# @!macro ivar_set_method
-
#
-
# @raise [Concurrent::PromiseExecutionError] if not the root promise
-
1
def set(value = NULL, &block)
-
raise PromiseExecutionError.new('supported only on root promise') unless root?
-
check_for_block_or_value!(block_given?, value)
-
synchronize do
-
if @state != :unscheduled
-
raise MultipleAssignmentError
-
else
-
@promise_body = block || Proc.new { |result| value }
-
end
-
end
-
execute
-
end
-
-
# @!macro ivar_fail_method
-
#
-
# @raise [Concurrent::PromiseExecutionError] if not the root promise
-
1
def fail(reason = StandardError.new)
-
set { raise reason }
-
end
-
-
# Create a new `Promise` object with the given block, execute it, and return the
-
# `:pending` object.
-
#
-
# @!macro executor_and_deref_options
-
#
-
# @!macro promise_init_options
-
#
-
# @return [Promise] the newly created `Promise` in the `:pending` state
-
#
-
# @raise [ArgumentError] if no block is given
-
#
-
# @example
-
# promise = Concurrent::Promise.execute{ sleep(1); 42 }
-
# promise.state #=> :pending
-
1
def self.execute(opts = {}, &block)
-
new(opts, &block).execute
-
end
-
-
# Chain a new promise off the current promise.
-
#
-
# @return [Promise] the new promise
-
# @yield The block operation to be performed asynchronously.
-
# @overload then(rescuer, executor, &block)
-
# @param [Proc] rescuer An optional rescue block to be executed if the
-
# promise is rejected.
-
# @param [ThreadPool] executor An optional thread pool executor to be used
-
# in the new Promise
-
# @overload then(rescuer, executor: executor, &block)
-
# @param [Proc] rescuer An optional rescue block to be executed if the
-
# promise is rejected.
-
# @param [ThreadPool] executor An optional thread pool executor to be used
-
# in the new Promise
-
1
def then(*args, &block)
-
if args.last.is_a?(::Hash)
-
executor = args.pop[:executor]
-
rescuer = args.first
-
else
-
rescuer, executor = args
-
end
-
-
executor ||= @executor
-
-
raise ArgumentError.new('rescuers and block are both missing') if rescuer.nil? && !block_given?
-
block = Proc.new { |result| result } unless block_given?
-
child = Promise.new(
-
parent: self,
-
executor: executor,
-
on_fulfill: block,
-
on_reject: rescuer
-
)
-
-
synchronize do
-
child.state = :pending if @state == :pending
-
child.on_fulfill(apply_deref_options(@value)) if @state == :fulfilled
-
child.on_reject(@reason) if @state == :rejected
-
@children << child
-
end
-
-
child
-
end
-
-
# Chain onto this promise an action to be undertaken on success
-
# (fulfillment).
-
#
-
# @yield The block to execute
-
#
-
# @return [Promise] self
-
1
def on_success(&block)
-
raise ArgumentError.new('no block given') unless block_given?
-
self.then(&block)
-
end
-
-
# Chain onto this promise an action to be undertaken on failure
-
# (rejection).
-
#
-
# @yield The block to execute
-
#
-
# @return [Promise] self
-
1
def rescue(&block)
-
self.then(block)
-
end
-
-
1
alias_method :catch, :rescue
-
1
alias_method :on_error, :rescue
-
-
# Yield the successful result to the block that returns a promise. If that
-
# promise is also successful the result is the result of the yielded promise.
-
# If either part fails the whole also fails.
-
#
-
# @example
-
# Promise.execute { 1 }.flat_map { |v| Promise.execute { v + 2 } }.value! #=> 3
-
#
-
# @return [Promise]
-
1
def flat_map(&block)
-
child = Promise.new(
-
parent: self,
-
executor: ImmediateExecutor.new,
-
)
-
-
on_error { |e| child.on_reject(e) }
-
on_success do |result1|
-
begin
-
inner = block.call(result1)
-
inner.execute
-
inner.on_success { |result2| child.on_fulfill(result2) }
-
inner.on_error { |e| child.on_reject(e) }
-
rescue => e
-
child.on_reject(e)
-
end
-
end
-
-
child
-
end
-
-
# Builds a promise that produces the result of promises in an Array
-
# and fails if any of them fails.
-
#
-
# @overload zip(*promises)
-
# @param [Array<Promise>] promises
-
#
-
# @overload zip(*promises, opts)
-
# @param [Array<Promise>] promises
-
# @param [Hash] opts the configuration options
-
# @option opts [Executor] :executor (ImmediateExecutor.new) when set use the given `Executor` instance.
-
# @option opts [Boolean] :execute (true) execute promise before returning
-
#
-
# @return [Promise<Array>]
-
1
def self.zip(*promises)
-
opts = promises.last.is_a?(::Hash) ? promises.pop.dup : {}
-
opts[:executor] ||= ImmediateExecutor.new
-
zero = if !opts.key?(:execute) || opts.delete(:execute)
-
fulfill([], opts)
-
else
-
Promise.new(opts) { [] }
-
end
-
-
promises.reduce(zero) do |p1, p2|
-
p1.flat_map do |results|
-
p2.then do |next_result|
-
results << next_result
-
end
-
end
-
end
-
end
-
-
# Builds a promise that produces the result of self and others in an Array
-
# and fails if any of them fails.
-
#
-
# @overload zip(*promises)
-
# @param [Array<Promise>] others
-
#
-
# @overload zip(*promises, opts)
-
# @param [Array<Promise>] others
-
# @param [Hash] opts the configuration options
-
# @option opts [Executor] :executor (ImmediateExecutor.new) when set use the given `Executor` instance.
-
# @option opts [Boolean] :execute (true) execute promise before returning
-
#
-
# @return [Promise<Array>]
-
1
def zip(*others)
-
self.class.zip(self, *others)
-
end
-
-
# Aggregates a collection of promises and executes the `then` condition
-
# if all aggregated promises succeed. Executes the `rescue` handler with
-
# a `Concurrent::PromiseExecutionError` if any of the aggregated promises
-
# fail. Upon execution will execute any of the aggregate promises that
-
# were not already executed.
-
#
-
# @!macro promise_self_aggregate
-
#
-
# The returned promise will not yet have been executed. Additional `#then`
-
# and `#rescue` handlers may still be provided. Once the returned promise
-
# is execute the aggregate promises will be also be executed (if they have
-
# not been executed already). The results of the aggregate promises will
-
# be checked upon completion. The necessary `#then` and `#rescue` blocks
-
# on the aggregating promise will then be executed as appropriate. If the
-
# `#rescue` handlers are executed the raises exception will be
-
# `Concurrent::PromiseExecutionError`.
-
#
-
# @param [Array] promises Zero or more promises to aggregate
-
# @return [Promise] an unscheduled (not executed) promise that aggregates
-
# the promises given as arguments
-
1
def self.all?(*promises)
-
aggregate(:all?, *promises)
-
end
-
-
# Aggregates a collection of promises and executes the `then` condition
-
# if any aggregated promises succeed. Executes the `rescue` handler with
-
# a `Concurrent::PromiseExecutionError` if any of the aggregated promises
-
# fail. Upon execution will execute any of the aggregate promises that
-
# were not already executed.
-
#
-
# @!macro promise_self_aggregate
-
1
def self.any?(*promises)
-
aggregate(:any?, *promises)
-
end
-
-
1
protected
-
-
1
def ns_initialize(value, opts)
-
super
-
-
@executor = Options.executor_from_options(opts) || Concurrent.global_io_executor
-
@args = get_arguments_from(opts)
-
-
@parent = opts.fetch(:parent) { nil }
-
@on_fulfill = opts.fetch(:on_fulfill) { Proc.new { |result| result } }
-
@on_reject = opts.fetch(:on_reject) { Proc.new { |reason| raise reason } }
-
-
@promise_body = opts[:__promise_body_from_block__] || Proc.new { |result| result }
-
@state = :unscheduled
-
@children = []
-
end
-
-
# Aggregate a collection of zero or more promises under a composite promise,
-
# execute the aggregated promises and collect them into a standard Ruby array,
-
# call the given Ruby `Ennnumerable` predicate (such as `any?`, `all?`, `none?`,
-
# or `one?`) on the collection checking for the success or failure of each,
-
# then executing the composite's `#then` handlers if the predicate returns
-
# `true` or executing the composite's `#rescue` handlers if the predicate
-
# returns false.
-
#
-
# @!macro promise_self_aggregate
-
1
def self.aggregate(method, *promises)
-
composite = Promise.new do
-
completed = promises.collect do |promise|
-
promise.execute if promise.unscheduled?
-
promise.wait
-
promise
-
end
-
unless completed.empty? || completed.send(method){|promise| promise.fulfilled? }
-
raise PromiseExecutionError
-
end
-
end
-
composite
-
end
-
-
# @!visibility private
-
1
def set_pending
-
synchronize do
-
@state = :pending
-
@children.each { |c| c.set_pending }
-
end
-
end
-
-
# @!visibility private
-
1
def root? # :nodoc:
-
@parent.nil?
-
end
-
-
# @!visibility private
-
1
def on_fulfill(result)
-
realize Proc.new { @on_fulfill.call(result) }
-
nil
-
end
-
-
# @!visibility private
-
1
def on_reject(reason)
-
realize Proc.new { @on_reject.call(reason) }
-
nil
-
end
-
-
# @!visibility private
-
1
def notify_child(child)
-
if_state(:fulfilled) { child.on_fulfill(apply_deref_options(@value)) }
-
if_state(:rejected) { child.on_reject(@reason) }
-
end
-
-
# @!visibility private
-
1
def complete(success, value, reason)
-
children_to_notify = synchronize do
-
set_state!(success, value, reason)
-
@children.dup
-
end
-
-
children_to_notify.each { |child| notify_child(child) }
-
observers.notify_and_delete_observers{ [Time.now, self.value, reason] }
-
end
-
-
# @!visibility private
-
1
def realize(task)
-
@executor.post do
-
success, value, reason = SafeTaskExecutor.new(task, rescue_exception: true).execute(*@args)
-
complete(success, value, reason)
-
end
-
end
-
-
# @!visibility private
-
1
def set_state!(success, value, reason)
-
set_state(success, value, reason)
-
event.set
-
end
-
-
# @!visibility private
-
1
def synchronized_set_state!(success, value, reason)
-
synchronize { set_state!(success, value, reason) }
-
end
-
end
-
end
-
1
require 'concurrent/synchronization'
-
1
require 'concurrent/atomic/atomic_boolean'
-
1
require 'concurrent/atomic/atomic_fixnum'
-
1
require 'concurrent/collection/lock_free_stack'
-
1
require 'concurrent/errors'
-
1
require 'concurrent/re_include'
-
-
1
module Concurrent
-
-
# {include:file:docs-source/promises-main.md}
-
1
module Promises
-
-
# @!macro promises.param.default_executor
-
# @param [Executor, :io, :fast] default_executor Instance of an executor or a name of the
-
# global executor. Default executor propagates to chained futures unless overridden with
-
# executor parameter or changed with {AbstractEventFuture#with_default_executor}.
-
#
-
# @!macro promises.param.executor
-
# @param [Executor, :io, :fast] executor Instance of an executor or a name of the
-
# global executor. The task is executed on it, default executor remains unchanged.
-
#
-
# @!macro promises.param.args
-
# @param [Object] args arguments which are passed to the task when it's executed.
-
# (It might be prepended with other arguments, see the @yeild section).
-
#
-
# @!macro promises.shortcut.on
-
# Shortcut of {#$0_on} with default `:io` executor supplied.
-
# @see #$0_on
-
#
-
# @!macro promises.shortcut.using
-
# Shortcut of {#$0_using} with default `:io` executor supplied.
-
# @see #$0_using
-
#
-
# @!macro promise.param.task-future
-
# @yieldreturn will become result of the returned Future.
-
# Its returned value becomes {Future#value} fulfilling it,
-
# raised exception becomes {Future#reason} rejecting it.
-
#
-
# @!macro promise.param.callback
-
# @yieldreturn is forgotten.
-
-
# Container of all {Future}, {Event} factory methods. They are never constructed directly with
-
# new.
-
1
module FactoryMethods
-
1
extend ReInclude
-
1
extend self
-
-
1
module Configuration
-
# @return [Executor, :io, :fast] the executor which is used when none is supplied
-
# to a factory method. The method can be overridden in the receivers of
-
# `include FactoryMethod`
-
1
def default_executor
-
:io
-
end
-
end
-
-
1
include Configuration
-
-
# @!macro promises.shortcut.on
-
# @return [ResolvableEvent]
-
1
def resolvable_event
-
resolvable_event_on default_executor
-
end
-
-
# Created resolvable event, user is responsible for resolving the event once by
-
# {Promises::ResolvableEvent#resolve}.
-
#
-
# @!macro promises.param.default_executor
-
# @return [ResolvableEvent]
-
1
def resolvable_event_on(default_executor = self.default_executor)
-
ResolvableEventPromise.new(default_executor).future
-
end
-
-
# @!macro promises.shortcut.on
-
# @return [ResolvableFuture]
-
1
def resolvable_future
-
resolvable_future_on default_executor
-
end
-
-
# Creates resolvable future, user is responsible for resolving the future once by
-
# {Promises::ResolvableFuture#resolve}, {Promises::ResolvableFuture#fulfill},
-
# or {Promises::ResolvableFuture#reject}
-
#
-
# @!macro promises.param.default_executor
-
# @return [ResolvableFuture]
-
1
def resolvable_future_on(default_executor = self.default_executor)
-
ResolvableFuturePromise.new(default_executor).future
-
end
-
-
# @!macro promises.shortcut.on
-
# @return [Future]
-
1
def future(*args, &task)
-
future_on(default_executor, *args, &task)
-
end
-
-
# Constructs new Future which will be resolved after block is evaluated on default executor.
-
# Evaluation begins immediately.
-
#
-
# @!macro promises.param.default_executor
-
# @!macro promises.param.args
-
# @yield [*args] to the task.
-
# @!macro promise.param.task-future
-
# @return [Future]
-
1
def future_on(default_executor, *args, &task)
-
ImmediateEventPromise.new(default_executor).future.then(*args, &task)
-
end
-
-
# Creates resolved future with will be either fulfilled with the given value or rejection with
-
# the given reason.
-
#
-
# @param [true, false] fulfilled
-
# @param [Object] value
-
# @param [Object] reason
-
# @!macro promises.param.default_executor
-
# @return [Future]
-
1
def resolved_future(fulfilled, value, reason, default_executor = self.default_executor)
-
ImmediateFuturePromise.new(default_executor, fulfilled, value, reason).future
-
end
-
-
# Creates resolved future with will be fulfilled with the given value.
-
#
-
# @!macro promises.param.default_executor
-
# @param [Object] value
-
# @return [Future]
-
1
def fulfilled_future(value, default_executor = self.default_executor)
-
resolved_future true, value, nil, default_executor
-
end
-
-
# Creates resolved future with will be rejected with the given reason.
-
#
-
# @!macro promises.param.default_executor
-
# @param [Object] reason
-
# @return [Future]
-
1
def rejected_future(reason, default_executor = self.default_executor)
-
resolved_future false, nil, reason, default_executor
-
end
-
-
# Creates resolved event.
-
#
-
# @!macro promises.param.default_executor
-
# @return [Event]
-
1
def resolved_event(default_executor = self.default_executor)
-
ImmediateEventPromise.new(default_executor).event
-
end
-
-
# General constructor. Behaves differently based on the argument's type. It's provided for convenience
-
# but it's better to be explicit.
-
#
-
# @see rejected_future, resolved_event, fulfilled_future
-
# @!macro promises.param.default_executor
-
# @return [Event, Future]
-
#
-
# @overload make_future(nil, default_executor = self.default_executor)
-
# @param [nil] nil
-
# @return [Event] resolved event.
-
#
-
# @overload make_future(a_future, default_executor = self.default_executor)
-
# @param [Future] a_future
-
# @return [Future] a future which will be resolved when a_future is.
-
#
-
# @overload make_future(an_event, default_executor = self.default_executor)
-
# @param [Event] an_event
-
# @return [Event] an event which will be resolved when an_event is.
-
#
-
# @overload make_future(exception, default_executor = self.default_executor)
-
# @param [Exception] exception
-
# @return [Future] a rejected future with the exception as its reason.
-
#
-
# @overload make_future(value, default_executor = self.default_executor)
-
# @param [Object] value when none of the above overloads fits
-
# @return [Future] a fulfilled future with the value.
-
1
def make_future(argument = nil, default_executor = self.default_executor)
-
case argument
-
when AbstractEventFuture
-
# returning wrapper would change nothing
-
argument
-
when Exception
-
rejected_future argument, default_executor
-
when nil
-
resolved_event default_executor
-
else
-
fulfilled_future argument, default_executor
-
end
-
end
-
-
# @!macro promises.shortcut.on
-
# @return [Future, Event]
-
1
def delay(*args, &task)
-
delay_on default_executor, *args, &task
-
end
-
-
# Creates new event or future which is resolved only after it is touched,
-
# see {Concurrent::AbstractEventFuture#touch}.
-
#
-
# @!macro promises.param.default_executor
-
# @overload delay_on(default_executor, *args, &task)
-
# If task is provided it returns a {Future} representing the result of the task.
-
# @!macro promises.param.args
-
# @yield [*args] to the task.
-
# @!macro promise.param.task-future
-
# @return [Future]
-
# @overload delay_on(default_executor)
-
# If no task is provided, it returns an {Event}
-
# @return [Event]
-
1
def delay_on(default_executor, *args, &task)
-
event = DelayPromise.new(default_executor).event
-
task ? event.chain(*args, &task) : event
-
end
-
-
# @!macro promises.shortcut.on
-
# @return [Future, Event]
-
1
def schedule(intended_time, *args, &task)
-
schedule_on default_executor, intended_time, *args, &task
-
end
-
-
# Creates new event or future which is resolved in intended_time.
-
#
-
# @!macro promises.param.default_executor
-
# @!macro promises.param.intended_time
-
# @param [Numeric, Time] intended_time `Numeric` means to run in `intended_time` seconds.
-
# `Time` means to run on `intended_time`.
-
# @overload schedule_on(default_executor, intended_time, *args, &task)
-
# If task is provided it returns a {Future} representing the result of the task.
-
# @!macro promises.param.args
-
# @yield [*args] to the task.
-
# @!macro promise.param.task-future
-
# @return [Future]
-
# @overload schedule_on(default_executor, intended_time)
-
# If no task is provided, it returns an {Event}
-
# @return [Event]
-
1
def schedule_on(default_executor, intended_time, *args, &task)
-
event = ScheduledPromise.new(default_executor, intended_time).event
-
task ? event.chain(*args, &task) : event
-
end
-
-
# @!macro promises.shortcut.on
-
# @return [Future]
-
1
def zip_futures(*futures_and_or_events)
-
zip_futures_on default_executor, *futures_and_or_events
-
end
-
-
# Creates new future which is resolved after all futures_and_or_events are resolved.
-
# Its value is array of zipped future values. Its reason is array of reasons for rejection.
-
# If there is an error it rejects.
-
# @!macro promises.event-conversion
-
# If event is supplied, which does not have value and can be only resolved, it's
-
# represented as `:fulfilled` with value `nil`.
-
#
-
# @!macro promises.param.default_executor
-
# @param [AbstractEventFuture] futures_and_or_events
-
# @return [Future]
-
1
def zip_futures_on(default_executor, *futures_and_or_events)
-
ZipFuturesPromise.new_blocked_by(futures_and_or_events, default_executor).future
-
end
-
-
1
alias_method :zip, :zip_futures
-
-
# @!macro promises.shortcut.on
-
# @return [Event]
-
1
def zip_events(*futures_and_or_events)
-
zip_events_on default_executor, *futures_and_or_events
-
end
-
-
# Creates new event which is resolved after all futures_and_or_events are resolved.
-
# (Future is resolved when fulfilled or rejected.)
-
#
-
# @!macro promises.param.default_executor
-
# @param [AbstractEventFuture] futures_and_or_events
-
# @return [Event]
-
1
def zip_events_on(default_executor, *futures_and_or_events)
-
ZipEventsPromise.new_blocked_by(futures_and_or_events, default_executor).event
-
end
-
-
# @!macro promises.shortcut.on
-
# @return [Future]
-
1
def any_resolved_future(*futures_and_or_events)
-
any_resolved_future_on default_executor, *futures_and_or_events
-
end
-
-
1
alias_method :any, :any_resolved_future
-
-
# Creates new future which is resolved after first futures_and_or_events is resolved.
-
# Its result equals result of the first resolved future.
-
# @!macro promises.any-touch
-
# If resolved it does not propagate {Concurrent::AbstractEventFuture#touch}, leaving delayed
-
# futures un-executed if they are not required any more.
-
# @!macro promises.event-conversion
-
#
-
# @!macro promises.param.default_executor
-
# @param [AbstractEventFuture] futures_and_or_events
-
# @return [Future]
-
1
def any_resolved_future_on(default_executor, *futures_and_or_events)
-
AnyResolvedFuturePromise.new_blocked_by(futures_and_or_events, default_executor).future
-
end
-
-
# @!macro promises.shortcut.on
-
# @return [Future]
-
1
def any_fulfilled_future(*futures_and_or_events)
-
any_fulfilled_future_on default_executor, *futures_and_or_events
-
end
-
-
# Creates new future which is resolved after first of futures_and_or_events is fulfilled.
-
# Its result equals result of the first resolved future or if all futures_and_or_events reject,
-
# it has reason of the last resolved future.
-
# @!macro promises.any-touch
-
# @!macro promises.event-conversion
-
#
-
# @!macro promises.param.default_executor
-
# @param [AbstractEventFuture] futures_and_or_events
-
# @return [Future]
-
1
def any_fulfilled_future_on(default_executor, *futures_and_or_events)
-
AnyFulfilledFuturePromise.new_blocked_by(futures_and_or_events, default_executor).future
-
end
-
-
# @!macro promises.shortcut.on
-
# @return [Future]
-
1
def any_event(*futures_and_or_events)
-
any_event_on default_executor, *futures_and_or_events
-
end
-
-
# Creates new event which becomes resolved after first of the futures_and_or_events resolves.
-
# @!macro promises.any-touch
-
#
-
# @!macro promises.param.default_executor
-
# @param [AbstractEventFuture] futures_and_or_events
-
# @return [Event]
-
1
def any_event_on(default_executor, *futures_and_or_events)
-
AnyResolvedEventPromise.new_blocked_by(futures_and_or_events, default_executor).event
-
end
-
-
# TODO consider adding first(count, *futures)
-
# TODO consider adding zip_by(slice, *futures) processing futures in slices
-
# TODO or rather a generic aggregator taking a function
-
end
-
-
1
module InternalStates
-
# @!visibility private
-
1
class State
-
1
def resolved?
-
raise NotImplementedError
-
end
-
-
1
def to_sym
-
raise NotImplementedError
-
end
-
end
-
-
# @!visibility private
-
1
class Pending < State
-
1
def resolved?
-
false
-
end
-
-
1
def to_sym
-
:pending
-
end
-
end
-
-
# @!visibility private
-
1
class Reserved < Pending
-
end
-
-
# @!visibility private
-
1
class ResolvedWithResult < State
-
1
def resolved?
-
true
-
end
-
-
1
def to_sym
-
:resolved
-
end
-
-
1
def result
-
[fulfilled?, value, reason]
-
end
-
-
1
def fulfilled?
-
raise NotImplementedError
-
end
-
-
1
def value
-
raise NotImplementedError
-
end
-
-
1
def reason
-
raise NotImplementedError
-
end
-
-
1
def apply
-
raise NotImplementedError
-
end
-
end
-
-
# @!visibility private
-
1
class Fulfilled < ResolvedWithResult
-
-
1
def initialize(value)
-
1
@Value = value
-
end
-
-
1
def fulfilled?
-
true
-
end
-
-
1
def apply(args, block)
-
block.call value, *args
-
end
-
-
1
def value
-
@Value
-
end
-
-
1
def reason
-
nil
-
end
-
-
1
def to_sym
-
:fulfilled
-
end
-
end
-
-
# @!visibility private
-
1
class FulfilledArray < Fulfilled
-
1
def apply(args, block)
-
block.call(*value, *args)
-
end
-
end
-
-
# @!visibility private
-
1
class Rejected < ResolvedWithResult
-
1
def initialize(reason)
-
@Reason = reason
-
end
-
-
1
def fulfilled?
-
false
-
end
-
-
1
def value
-
nil
-
end
-
-
1
def reason
-
@Reason
-
end
-
-
1
def to_sym
-
:rejected
-
end
-
-
1
def apply(args, block)
-
block.call reason, *args
-
end
-
end
-
-
# @!visibility private
-
1
class PartiallyRejected < ResolvedWithResult
-
1
def initialize(value, reason)
-
super()
-
@Value = value
-
@Reason = reason
-
end
-
-
1
def fulfilled?
-
false
-
end
-
-
1
def to_sym
-
:rejected
-
end
-
-
1
def value
-
@Value
-
end
-
-
1
def reason
-
@Reason
-
end
-
-
1
def apply(args, block)
-
block.call(*reason, *args)
-
end
-
end
-
-
# @!visibility private
-
1
PENDING = Pending.new
-
# @!visibility private
-
1
RESERVED = Reserved.new
-
# @!visibility private
-
1
RESOLVED = Fulfilled.new(nil)
-
-
1
def RESOLVED.to_sym
-
:resolved
-
end
-
end
-
-
1
private_constant :InternalStates
-
-
# @!macro promises.shortcut.event-future
-
# @see Event#$0
-
# @see Future#$0
-
-
# @!macro promises.param.timeout
-
# @param [Numeric] timeout the maximum time in second to wait.
-
-
# @!macro promises.warn.blocks
-
# @note This function potentially blocks current thread until the Future is resolved.
-
# Be careful it can deadlock. Try to chain instead.
-
-
# Common ancestor of {Event} and {Future} classes, many shared methods are defined here.
-
1
class AbstractEventFuture < Synchronization::Object
-
1
safe_initialization!
-
1
attr_atomic(:internal_state)
-
1
private :internal_state=, :swap_internal_state, :compare_and_set_internal_state, :update_internal_state
-
# @!method internal_state
-
# @!visibility private
-
-
1
include InternalStates
-
-
1
def initialize(promise, default_executor)
-
super()
-
@Lock = Mutex.new
-
@Condition = ConditionVariable.new
-
@Promise = promise
-
@DefaultExecutor = default_executor
-
@Callbacks = LockFreeStack.new
-
@Waiters = AtomicFixnum.new 0
-
self.internal_state = PENDING
-
end
-
-
1
private :initialize
-
-
# Returns its state.
-
# @return [Symbol]
-
#
-
# @overload an_event.state
-
# @return [:pending, :resolved]
-
# @overload a_future.state
-
# Both :fulfilled, :rejected implies :resolved.
-
# @return [:pending, :fulfilled, :rejected]
-
1
def state
-
internal_state.to_sym
-
end
-
-
# Is it in pending state?
-
# @return [Boolean]
-
1
def pending?
-
!internal_state.resolved?
-
end
-
-
# Is it in resolved state?
-
# @return [Boolean]
-
1
def resolved?
-
internal_state.resolved?
-
end
-
-
# Propagates touch. Requests all the delayed futures, which it depends on, to be
-
# executed. This method is called by any other method requiring resolved state, like {#wait}.
-
# @return [self]
-
1
def touch
-
@Promise.touch
-
self
-
end
-
-
# @!macro promises.touches
-
# Calls {Concurrent::AbstractEventFuture#touch}.
-
-
# @!macro promises.method.wait
-
# Wait (block the Thread) until receiver is {#resolved?}.
-
# @!macro promises.touches
-
#
-
# @!macro promises.warn.blocks
-
# @!macro promises.param.timeout
-
# @return [self, true, false] self implies timeout was not used, true implies timeout was used
-
# and it was resolved, false implies it was not resolved within timeout.
-
1
def wait(timeout = nil)
-
result = wait_until_resolved(timeout)
-
timeout ? result : self
-
end
-
-
# Returns default executor.
-
# @return [Executor] default executor
-
# @see #with_default_executor
-
# @see FactoryMethods#future_on
-
# @see FactoryMethods#resolvable_future
-
# @see FactoryMethods#any_fulfilled_future_on
-
# @see similar
-
1
def default_executor
-
@DefaultExecutor
-
end
-
-
# @!macro promises.shortcut.on
-
# @return [Future]
-
1
def chain(*args, &task)
-
chain_on @DefaultExecutor, *args, &task
-
end
-
-
# Chains the task to be executed asynchronously on executor after it is resolved.
-
#
-
# @!macro promises.param.executor
-
# @!macro promises.param.args
-
# @return [Future]
-
# @!macro promise.param.task-future
-
#
-
# @overload an_event.chain_on(executor, *args, &task)
-
# @yield [*args] to the task.
-
# @overload a_future.chain_on(executor, *args, &task)
-
# @yield [fulfilled, value, reason, *args] to the task.
-
# @yieldparam [true, false] fulfilled
-
# @yieldparam [Object] value
-
# @yieldparam [Object] reason
-
1
def chain_on(executor, *args, &task)
-
ChainPromise.new_blocked_by1(self, @DefaultExecutor, executor, args, &task).future
-
end
-
-
# @return [String] Short string representation.
-
1
def to_s
-
format '%s %s>', super[0..-2], state
-
end
-
-
1
alias_method :inspect, :to_s
-
-
# Resolves the resolvable when receiver is resolved.
-
#
-
# @param [Resolvable] resolvable
-
# @return [self]
-
1
def chain_resolvable(resolvable)
-
on_resolution! { resolvable.resolve_with internal_state }
-
end
-
-
1
alias_method :tangle, :chain_resolvable
-
-
# @!macro promises.shortcut.using
-
# @return [self]
-
1
def on_resolution(*args, &callback)
-
on_resolution_using @DefaultExecutor, *args, &callback
-
end
-
-
# Stores the callback to be executed synchronously on resolving thread after it is
-
# resolved.
-
#
-
# @!macro promises.param.args
-
# @!macro promise.param.callback
-
# @return [self]
-
#
-
# @overload an_event.on_resolution!(*args, &callback)
-
# @yield [*args] to the callback.
-
# @overload a_future.on_resolution!(*args, &callback)
-
# @yield [fulfilled, value, reason, *args] to the callback.
-
# @yieldparam [true, false] fulfilled
-
# @yieldparam [Object] value
-
# @yieldparam [Object] reason
-
1
def on_resolution!(*args, &callback)
-
add_callback :callback_on_resolution, args, callback
-
end
-
-
# Stores the callback to be executed asynchronously on executor after it is resolved.
-
#
-
# @!macro promises.param.executor
-
# @!macro promises.param.args
-
# @!macro promise.param.callback
-
# @return [self]
-
#
-
# @overload an_event.on_resolution_using(executor, *args, &callback)
-
# @yield [*args] to the callback.
-
# @overload a_future.on_resolution_using(executor, *args, &callback)
-
# @yield [fulfilled, value, reason, *args] to the callback.
-
# @yieldparam [true, false] fulfilled
-
# @yieldparam [Object] value
-
# @yieldparam [Object] reason
-
1
def on_resolution_using(executor, *args, &callback)
-
add_callback :async_callback_on_resolution, executor, args, callback
-
end
-
-
# @!macro promises.method.with_default_executor
-
# Crates new object with same class with the executor set as its new default executor.
-
# Any futures depending on it will use the new default executor.
-
# @!macro promises.shortcut.event-future
-
# @abstract
-
# @return [AbstractEventFuture]
-
1
def with_default_executor(executor)
-
raise NotImplementedError
-
end
-
-
# @!visibility private
-
1
def resolve_with(state, raise_on_reassign = true, reserved = false)
-
if compare_and_set_internal_state(reserved ? RESERVED : PENDING, state)
-
# go to synchronized block only if there were waiting threads
-
@Lock.synchronize { @Condition.broadcast } unless @Waiters.value == 0
-
call_callbacks state
-
else
-
return rejected_resolution(raise_on_reassign, state)
-
end
-
self
-
end
-
-
# For inspection.
-
# @!visibility private
-
# @return [Array<AbstractPromise>]
-
1
def blocks
-
@Callbacks.each_with_object([]) do |(method, args), promises|
-
promises.push(args[0]) if method == :callback_notify_blocked
-
end
-
end
-
-
# For inspection.
-
# @!visibility private
-
1
def callbacks
-
@Callbacks.each.to_a
-
end
-
-
# For inspection.
-
# @!visibility private
-
1
def promise
-
@Promise
-
end
-
-
# For inspection.
-
# @!visibility private
-
1
def touched?
-
promise.touched?
-
end
-
-
# For inspection.
-
# @!visibility private
-
1
def waiting_threads
-
@Waiters.each.to_a
-
end
-
-
# @!visibility private
-
1
def add_callback_notify_blocked(promise, index)
-
add_callback :callback_notify_blocked, promise, index
-
end
-
-
# @!visibility private
-
1
def add_callback_clear_delayed_node(node)
-
add_callback(:callback_clear_delayed_node, node)
-
end
-
-
# @!visibility private
-
1
def with_hidden_resolvable
-
# TODO (pitr-ch 10-Dec-2018): documentation, better name if in edge
-
self
-
end
-
-
1
private
-
-
1
def add_callback(method, *args)
-
state = internal_state
-
if state.resolved?
-
call_callback method, state, args
-
else
-
@Callbacks.push [method, args]
-
state = internal_state
-
# take back if it was resolved in the meanwhile
-
call_callbacks state if state.resolved?
-
end
-
self
-
end
-
-
1
def callback_clear_delayed_node(state, node)
-
node.value = nil
-
end
-
-
# @return [Boolean]
-
1
def wait_until_resolved(timeout)
-
return true if resolved?
-
-
touch
-
-
@Lock.synchronize do
-
@Waiters.increment
-
begin
-
unless resolved?
-
@Condition.wait @Lock, timeout
-
end
-
ensure
-
# JRuby may raise ConcurrencyError
-
@Waiters.decrement
-
end
-
end
-
resolved?
-
end
-
-
1
def call_callback(method, state, args)
-
self.send method, state, *args
-
end
-
-
1
def call_callbacks(state)
-
method, args = @Callbacks.pop
-
while method
-
call_callback method, state, args
-
method, args = @Callbacks.pop
-
end
-
end
-
-
1
def with_async(executor, *args, &block)
-
Concurrent.executor(executor).post(*args, &block)
-
end
-
-
1
def async_callback_on_resolution(state, executor, args, callback)
-
with_async(executor, state, args, callback) do |st, ar, cb|
-
callback_on_resolution st, ar, cb
-
end
-
end
-
-
1
def callback_notify_blocked(state, promise, index)
-
promise.on_blocker_resolution self, index
-
end
-
end
-
-
# Represents an event which will happen in future (will be resolved). The event is either
-
# pending or resolved. It should be always resolved. Use {Future} to communicate rejections and
-
# cancellation.
-
1
class Event < AbstractEventFuture
-
-
1
alias_method :then, :chain
-
-
-
# @!macro promises.method.zip
-
# Creates a new event or a future which will be resolved when receiver and other are.
-
# Returns an event if receiver and other are events, otherwise returns a future.
-
# If just one of the parties is Future then the result
-
# of the returned future is equal to the result of the supplied future. If both are futures
-
# then the result is as described in {FactoryMethods#zip_futures_on}.
-
#
-
# @return [Future, Event]
-
1
def zip(other)
-
if other.is_a?(Future)
-
ZipFutureEventPromise.new_blocked_by2(other, self, @DefaultExecutor).future
-
else
-
ZipEventEventPromise.new_blocked_by2(self, other, @DefaultExecutor).event
-
end
-
end
-
-
1
alias_method :&, :zip
-
-
# Creates a new event which will be resolved when the first of receiver, `event_or_future`
-
# resolves.
-
#
-
# @return [Event]
-
1
def any(event_or_future)
-
AnyResolvedEventPromise.new_blocked_by2(self, event_or_future, @DefaultExecutor).event
-
end
-
-
1
alias_method :|, :any
-
-
# Creates new event dependent on receiver which will not evaluate until touched, see {#touch}.
-
# In other words, it inserts delay into the chain of Futures making rest of it lazy evaluated.
-
#
-
# @return [Event]
-
1
def delay
-
event = DelayPromise.new(@DefaultExecutor).event
-
ZipEventEventPromise.new_blocked_by2(self, event, @DefaultExecutor).event
-
end
-
-
# @!macro promise.method.schedule
-
# Creates new event dependent on receiver scheduled to execute on/in intended_time.
-
# In time is interpreted from the moment the receiver is resolved, therefore it inserts
-
# delay into the chain.
-
#
-
# @!macro promises.param.intended_time
-
# @return [Event]
-
1
def schedule(intended_time)
-
chain do
-
event = ScheduledPromise.new(@DefaultExecutor, intended_time).event
-
ZipEventEventPromise.new_blocked_by2(self, event, @DefaultExecutor).event
-
end.flat_event
-
end
-
-
# Converts event to a future. The future is fulfilled when the event is resolved, the future may never fail.
-
#
-
# @return [Future]
-
1
def to_future
-
future = Promises.resolvable_future
-
ensure
-
chain_resolvable(future)
-
end
-
-
# Returns self, since this is event
-
# @return [Event]
-
1
def to_event
-
self
-
end
-
-
# @!macro promises.method.with_default_executor
-
# @return [Event]
-
1
def with_default_executor(executor)
-
EventWrapperPromise.new_blocked_by1(self, executor).event
-
end
-
-
1
private
-
-
1
def rejected_resolution(raise_on_reassign, state)
-
Concurrent::MultipleAssignmentError.new('Event can be resolved only once') if raise_on_reassign
-
return false
-
end
-
-
1
def callback_on_resolution(state, args, callback)
-
callback.call(*args)
-
end
-
end
-
-
# Represents a value which will become available in future. May reject with a reason instead,
-
# e.g. when the tasks raises an exception.
-
1
class Future < AbstractEventFuture
-
-
# Is it in fulfilled state?
-
# @return [Boolean]
-
1
def fulfilled?
-
state = internal_state
-
state.resolved? && state.fulfilled?
-
end
-
-
# Is it in rejected state?
-
# @return [Boolean]
-
1
def rejected?
-
state = internal_state
-
state.resolved? && !state.fulfilled?
-
end
-
-
# @!macro promises.warn.nil
-
# @note Make sure returned `nil` is not confused with timeout, no value when rejected,
-
# no reason when fulfilled, etc.
-
# Use more exact methods if needed, like {#wait}, {#value!}, {#result}, etc.
-
-
# @!macro promises.method.value
-
# Return value of the future.
-
# @!macro promises.touches
-
#
-
# @!macro promises.warn.blocks
-
# @!macro promises.warn.nil
-
# @!macro promises.param.timeout
-
# @!macro promises.param.timeout_value
-
# @param [Object] timeout_value a value returned by the method when it times out
-
# @return [Object, nil, timeout_value] the value of the Future when fulfilled,
-
# timeout_value on timeout,
-
# nil on rejection.
-
1
def value(timeout = nil, timeout_value = nil)
-
if wait_until_resolved timeout
-
internal_state.value
-
else
-
timeout_value
-
end
-
end
-
-
# Returns reason of future's rejection.
-
# @!macro promises.touches
-
#
-
# @!macro promises.warn.blocks
-
# @!macro promises.warn.nil
-
# @!macro promises.param.timeout
-
# @!macro promises.param.timeout_value
-
# @return [Object, timeout_value] the reason, or timeout_value on timeout, or nil on fulfillment.
-
1
def reason(timeout = nil, timeout_value = nil)
-
if wait_until_resolved timeout
-
internal_state.reason
-
else
-
timeout_value
-
end
-
end
-
-
# Returns triplet fulfilled?, value, reason.
-
# @!macro promises.touches
-
#
-
# @!macro promises.warn.blocks
-
# @!macro promises.param.timeout
-
# @return [Array(Boolean, Object, Object), nil] triplet of fulfilled?, value, reason, or nil
-
# on timeout.
-
1
def result(timeout = nil)
-
internal_state.result if wait_until_resolved timeout
-
end
-
-
# @!macro promises.method.wait
-
# @raise [Exception] {#reason} on rejection
-
1
def wait!(timeout = nil)
-
result = wait_until_resolved!(timeout)
-
timeout ? result : self
-
end
-
-
# @!macro promises.method.value
-
# @return [Object, nil, timeout_value] the value of the Future when fulfilled,
-
# or nil on rejection,
-
# or timeout_value on timeout.
-
# @raise [Exception] {#reason} on rejection
-
1
def value!(timeout = nil, timeout_value = nil)
-
if wait_until_resolved! timeout
-
internal_state.value
-
else
-
timeout_value
-
end
-
end
-
-
# Allows rejected Future to be risen with `raise` method.
-
# If the reason is not an exception `Runtime.new(reason)` is returned.
-
#
-
# @example
-
# raise Promises.rejected_future(StandardError.new("boom"))
-
# raise Promises.rejected_future("or just boom")
-
# @raise [Concurrent::Error] when raising not rejected future
-
# @return [Exception]
-
1
def exception(*args)
-
raise Concurrent::Error, 'it is not rejected' unless rejected?
-
raise ArgumentError unless args.size <= 1
-
reason = Array(internal_state.reason).flatten.compact
-
if reason.size > 1
-
ex = Concurrent::MultipleErrors.new reason
-
ex.set_backtrace(caller)
-
ex
-
else
-
ex = if reason[0].respond_to? :exception
-
reason[0].exception(*args)
-
else
-
RuntimeError.new(reason[0]).exception(*args)
-
end
-
ex.set_backtrace Array(ex.backtrace) + caller
-
ex
-
end
-
end
-
-
# @!macro promises.shortcut.on
-
# @return [Future]
-
1
def then(*args, &task)
-
then_on @DefaultExecutor, *args, &task
-
end
-
-
# Chains the task to be executed asynchronously on executor after it fulfills. Does not run
-
# the task if it rejects. It will resolve though, triggering any dependent futures.
-
#
-
# @!macro promises.param.executor
-
# @!macro promises.param.args
-
# @!macro promise.param.task-future
-
# @return [Future]
-
# @yield [value, *args] to the task.
-
1
def then_on(executor, *args, &task)
-
ThenPromise.new_blocked_by1(self, @DefaultExecutor, executor, args, &task).future
-
end
-
-
# @!macro promises.shortcut.on
-
# @return [Future]
-
1
def rescue(*args, &task)
-
rescue_on @DefaultExecutor, *args, &task
-
end
-
-
# Chains the task to be executed asynchronously on executor after it rejects. Does not run
-
# the task if it fulfills. It will resolve though, triggering any dependent futures.
-
#
-
# @!macro promises.param.executor
-
# @!macro promises.param.args
-
# @!macro promise.param.task-future
-
# @return [Future]
-
# @yield [reason, *args] to the task.
-
1
def rescue_on(executor, *args, &task)
-
RescuePromise.new_blocked_by1(self, @DefaultExecutor, executor, args, &task).future
-
end
-
-
# @!macro promises.method.zip
-
# @return [Future]
-
1
def zip(other)
-
if other.is_a?(Future)
-
ZipFuturesPromise.new_blocked_by2(self, other, @DefaultExecutor).future
-
else
-
ZipFutureEventPromise.new_blocked_by2(self, other, @DefaultExecutor).future
-
end
-
end
-
-
1
alias_method :&, :zip
-
-
# Creates a new event which will be resolved when the first of receiver, `event_or_future`
-
# resolves. Returning future will have value nil if event_or_future is event and resolves
-
# first.
-
#
-
# @return [Future]
-
1
def any(event_or_future)
-
AnyResolvedFuturePromise.new_blocked_by2(self, event_or_future, @DefaultExecutor).future
-
end
-
-
1
alias_method :|, :any
-
-
# Creates new future dependent on receiver which will not evaluate until touched, see {#touch}.
-
# In other words, it inserts delay into the chain of Futures making rest of it lazy evaluated.
-
#
-
# @return [Future]
-
1
def delay
-
event = DelayPromise.new(@DefaultExecutor).event
-
ZipFutureEventPromise.new_blocked_by2(self, event, @DefaultExecutor).future
-
end
-
-
# @!macro promise.method.schedule
-
# @return [Future]
-
1
def schedule(intended_time)
-
chain do
-
event = ScheduledPromise.new(@DefaultExecutor, intended_time).event
-
ZipFutureEventPromise.new_blocked_by2(self, event, @DefaultExecutor).future
-
end.flat
-
end
-
-
# @!macro promises.method.with_default_executor
-
# @return [Future]
-
1
def with_default_executor(executor)
-
FutureWrapperPromise.new_blocked_by1(self, executor).future
-
end
-
-
# Creates new future which will have result of the future returned by receiver. If receiver
-
# rejects it will have its rejection.
-
#
-
# @param [Integer] level how many levels of futures should flatten
-
# @return [Future]
-
1
def flat_future(level = 1)
-
FlatFuturePromise.new_blocked_by1(self, level, @DefaultExecutor).future
-
end
-
-
1
alias_method :flat, :flat_future
-
-
# Creates new event which will be resolved when the returned event by receiver is.
-
# Be careful if the receiver rejects it will just resolve since Event does not hold reason.
-
#
-
# @return [Event]
-
1
def flat_event
-
FlatEventPromise.new_blocked_by1(self, @DefaultExecutor).event
-
end
-
-
# @!macro promises.shortcut.using
-
# @return [self]
-
1
def on_fulfillment(*args, &callback)
-
on_fulfillment_using @DefaultExecutor, *args, &callback
-
end
-
-
# Stores the callback to be executed synchronously on resolving thread after it is
-
# fulfilled. Does nothing on rejection.
-
#
-
# @!macro promises.param.args
-
# @!macro promise.param.callback
-
# @return [self]
-
# @yield [value, *args] to the callback.
-
1
def on_fulfillment!(*args, &callback)
-
add_callback :callback_on_fulfillment, args, callback
-
end
-
-
# Stores the callback to be executed asynchronously on executor after it is
-
# fulfilled. Does nothing on rejection.
-
#
-
# @!macro promises.param.executor
-
# @!macro promises.param.args
-
# @!macro promise.param.callback
-
# @return [self]
-
# @yield [value, *args] to the callback.
-
1
def on_fulfillment_using(executor, *args, &callback)
-
add_callback :async_callback_on_fulfillment, executor, args, callback
-
end
-
-
# @!macro promises.shortcut.using
-
# @return [self]
-
1
def on_rejection(*args, &callback)
-
on_rejection_using @DefaultExecutor, *args, &callback
-
end
-
-
# Stores the callback to be executed synchronously on resolving thread after it is
-
# rejected. Does nothing on fulfillment.
-
#
-
# @!macro promises.param.args
-
# @!macro promise.param.callback
-
# @return [self]
-
# @yield [reason, *args] to the callback.
-
1
def on_rejection!(*args, &callback)
-
add_callback :callback_on_rejection, args, callback
-
end
-
-
# Stores the callback to be executed asynchronously on executor after it is
-
# rejected. Does nothing on fulfillment.
-
#
-
# @!macro promises.param.executor
-
# @!macro promises.param.args
-
# @!macro promise.param.callback
-
# @return [self]
-
# @yield [reason, *args] to the callback.
-
1
def on_rejection_using(executor, *args, &callback)
-
add_callback :async_callback_on_rejection, executor, args, callback
-
end
-
-
# Allows to use futures as green threads. The receiver has to evaluate to a future which
-
# represents what should be done next. It basically flattens indefinitely until non Future
-
# values is returned which becomes result of the returned future. Any encountered exception
-
# will become reason of the returned future.
-
#
-
# @return [Future]
-
# @param [#call(value)] run_test
-
# an object which when called returns either Future to keep running with
-
# or nil, then the run completes with the value.
-
# The run_test can be used to extract the Future from deeper structure,
-
# or to distinguish Future which is a resulting value from a future
-
# which is suppose to continue running.
-
# @example
-
# body = lambda do |v|
-
# v += 1
-
# v < 5 ? Promises.future(v, &body) : v
-
# end
-
# Promises.future(0, &body).run.value! # => 5
-
1
def run(run_test = method(:run_test))
-
RunFuturePromise.new_blocked_by1(self, @DefaultExecutor, run_test).future
-
end
-
-
# @!visibility private
-
1
def apply(args, block)
-
internal_state.apply args, block
-
end
-
-
# Converts future to event which is resolved when future is resolved by fulfillment or rejection.
-
#
-
# @return [Event]
-
1
def to_event
-
event = Promises.resolvable_event
-
ensure
-
chain_resolvable(event)
-
end
-
-
# Returns self, since this is a future
-
# @return [Future]
-
1
def to_future
-
self
-
end
-
-
# @return [String] Short string representation.
-
1
def to_s
-
if resolved?
-
format '%s with %s>', super[0..-2], (fulfilled? ? value : reason).inspect
-
else
-
super
-
end
-
end
-
-
1
alias_method :inspect, :to_s
-
-
1
private
-
-
1
def run_test(v)
-
v if v.is_a?(Future)
-
end
-
-
1
def rejected_resolution(raise_on_reassign, state)
-
if raise_on_reassign
-
if internal_state == RESERVED
-
raise Concurrent::MultipleAssignmentError.new(
-
"Future can be resolved only once. It is already reserved.")
-
else
-
raise Concurrent::MultipleAssignmentError.new(
-
"Future can be resolved only once. It's #{result}, trying to set #{state.result}.",
-
current_result: result,
-
new_result: state.result)
-
end
-
end
-
return false
-
end
-
-
1
def wait_until_resolved!(timeout = nil)
-
result = wait_until_resolved(timeout)
-
raise self if rejected?
-
result
-
end
-
-
1
def async_callback_on_fulfillment(state, executor, args, callback)
-
with_async(executor, state, args, callback) do |st, ar, cb|
-
callback_on_fulfillment st, ar, cb
-
end
-
end
-
-
1
def async_callback_on_rejection(state, executor, args, callback)
-
with_async(executor, state, args, callback) do |st, ar, cb|
-
callback_on_rejection st, ar, cb
-
end
-
end
-
-
1
def callback_on_fulfillment(state, args, callback)
-
state.apply args, callback if state.fulfilled?
-
end
-
-
1
def callback_on_rejection(state, args, callback)
-
state.apply args, callback unless state.fulfilled?
-
end
-
-
1
def callback_on_resolution(state, args, callback)
-
callback.call(*state.result, *args)
-
end
-
-
end
-
-
# Marker module of Future, Event resolved manually.
-
1
module Resolvable
-
1
include InternalStates
-
end
-
-
# A Event which can be resolved by user.
-
1
class ResolvableEvent < Event
-
1
include Resolvable
-
-
# @!macro raise_on_reassign
-
# @raise [MultipleAssignmentError] when already resolved and raise_on_reassign is true.
-
-
# @!macro promise.param.raise_on_reassign
-
# @param [Boolean] raise_on_reassign should method raise exception if already resolved
-
# @return [self, false] false is returner when raise_on_reassign is false and the receiver
-
# is already resolved.
-
#
-
-
# Makes the event resolved, which triggers all dependent futures.
-
#
-
# @!macro promise.param.raise_on_reassign
-
# @!macro promise.param.reserved
-
# @param [true, false] reserved
-
# Set to true if the resolvable is {#reserve}d by you,
-
# marks resolution of reserved resolvable events and futures explicitly.
-
# Advanced feature, ignore unless you use {Resolvable#reserve} from edge.
-
1
def resolve(raise_on_reassign = true, reserved = false)
-
resolve_with RESOLVED, raise_on_reassign, reserved
-
end
-
-
# Creates new event wrapping receiver, effectively hiding the resolve method.
-
#
-
# @return [Event]
-
1
def with_hidden_resolvable
-
@with_hidden_resolvable ||= EventWrapperPromise.new_blocked_by1(self, @DefaultExecutor).event
-
end
-
-
# Behaves as {AbstractEventFuture#wait} but has one additional optional argument
-
# resolve_on_timeout.
-
#
-
# @param [true, false] resolve_on_timeout
-
# If it times out and the argument is true it will also resolve the event.
-
# @return [self, true, false]
-
# @see AbstractEventFuture#wait
-
1
def wait(timeout = nil, resolve_on_timeout = false)
-
super(timeout) or if resolve_on_timeout
-
# if it fails to resolve it was resolved in the meantime
-
# so return true as if there was no timeout
-
!resolve(false)
-
else
-
false
-
end
-
end
-
end
-
-
# A Future which can be resolved by user.
-
1
class ResolvableFuture < Future
-
1
include Resolvable
-
-
# Makes the future resolved with result of triplet `fulfilled?`, `value`, `reason`,
-
# which triggers all dependent futures.
-
#
-
# @param [true, false] fulfilled
-
# @param [Object] value
-
# @param [Object] reason
-
# @!macro promise.param.raise_on_reassign
-
# @!macro promise.param.reserved
-
1
def resolve(fulfilled = true, value = nil, reason = nil, raise_on_reassign = true, reserved = false)
-
resolve_with(fulfilled ? Fulfilled.new(value) : Rejected.new(reason), raise_on_reassign, reserved)
-
end
-
-
# Makes the future fulfilled with `value`,
-
# which triggers all dependent futures.
-
#
-
# @param [Object] value
-
# @!macro promise.param.raise_on_reassign
-
# @!macro promise.param.reserved
-
1
def fulfill(value, raise_on_reassign = true, reserved = false)
-
resolve_with Fulfilled.new(value), raise_on_reassign, reserved
-
end
-
-
# Makes the future rejected with `reason`,
-
# which triggers all dependent futures.
-
#
-
# @param [Object] reason
-
# @!macro promise.param.raise_on_reassign
-
# @!macro promise.param.reserved
-
1
def reject(reason, raise_on_reassign = true, reserved = false)
-
resolve_with Rejected.new(reason), raise_on_reassign, reserved
-
end
-
-
# Evaluates the block and sets its result as future's value fulfilling, if the block raises
-
# an exception the future rejects with it.
-
#
-
# @yield [*args] to the block.
-
# @yieldreturn [Object] value
-
# @return [self]
-
1
def evaluate_to(*args, &block)
-
promise.evaluate_to(*args, block)
-
end
-
-
# Evaluates the block and sets its result as future's value fulfilling, if the block raises
-
# an exception the future rejects with it.
-
#
-
# @yield [*args] to the block.
-
# @yieldreturn [Object] value
-
# @return [self]
-
# @raise [Exception] also raise reason on rejection.
-
1
def evaluate_to!(*args, &block)
-
promise.evaluate_to(*args, block).wait!
-
end
-
-
# @!macro promises.resolvable.resolve_on_timeout
-
# @param [::Array(true, Object, nil), ::Array(false, nil, Exception), nil] resolve_on_timeout
-
# If it times out and the argument is not nil it will also resolve the future
-
# to the provided resolution.
-
-
# Behaves as {AbstractEventFuture#wait} but has one additional optional argument
-
# resolve_on_timeout.
-
#
-
# @!macro promises.resolvable.resolve_on_timeout
-
# @return [self, true, false]
-
# @see AbstractEventFuture#wait
-
1
def wait(timeout = nil, resolve_on_timeout = nil)
-
super(timeout) or if resolve_on_timeout
-
# if it fails to resolve it was resolved in the meantime
-
# so return true as if there was no timeout
-
!resolve(*resolve_on_timeout, false)
-
else
-
false
-
end
-
end
-
-
# Behaves as {Future#wait!} but has one additional optional argument
-
# resolve_on_timeout.
-
#
-
# @!macro promises.resolvable.resolve_on_timeout
-
# @return [self, true, false]
-
# @raise [Exception] {#reason} on rejection
-
# @see Future#wait!
-
1
def wait!(timeout = nil, resolve_on_timeout = nil)
-
super(timeout) or if resolve_on_timeout
-
if resolve(*resolve_on_timeout, false)
-
false
-
else
-
# if it fails to resolve it was resolved in the meantime
-
# so return true as if there was no timeout
-
raise self if rejected?
-
true
-
end
-
else
-
false
-
end
-
end
-
-
# Behaves as {Future#value} but has one additional optional argument
-
# resolve_on_timeout.
-
#
-
# @!macro promises.resolvable.resolve_on_timeout
-
# @return [Object, timeout_value, nil]
-
# @see Future#value
-
1
def value(timeout = nil, timeout_value = nil, resolve_on_timeout = nil)
-
if wait_until_resolved timeout
-
internal_state.value
-
else
-
if resolve_on_timeout
-
unless resolve(*resolve_on_timeout, false)
-
# if it fails to resolve it was resolved in the meantime
-
# so return value as if there was no timeout
-
return internal_state.value
-
end
-
end
-
timeout_value
-
end
-
end
-
-
# Behaves as {Future#value!} but has one additional optional argument
-
# resolve_on_timeout.
-
#
-
# @!macro promises.resolvable.resolve_on_timeout
-
# @return [Object, timeout_value, nil]
-
# @raise [Exception] {#reason} on rejection
-
# @see Future#value!
-
1
def value!(timeout = nil, timeout_value = nil, resolve_on_timeout = nil)
-
if wait_until_resolved! timeout
-
internal_state.value
-
else
-
if resolve_on_timeout
-
unless resolve(*resolve_on_timeout, false)
-
# if it fails to resolve it was resolved in the meantime
-
# so return value as if there was no timeout
-
raise self if rejected?
-
return internal_state.value
-
end
-
end
-
timeout_value
-
end
-
end
-
-
# Behaves as {Future#reason} but has one additional optional argument
-
# resolve_on_timeout.
-
#
-
# @!macro promises.resolvable.resolve_on_timeout
-
# @return [Exception, timeout_value, nil]
-
# @see Future#reason
-
1
def reason(timeout = nil, timeout_value = nil, resolve_on_timeout = nil)
-
if wait_until_resolved timeout
-
internal_state.reason
-
else
-
if resolve_on_timeout
-
unless resolve(*resolve_on_timeout, false)
-
# if it fails to resolve it was resolved in the meantime
-
# so return value as if there was no timeout
-
return internal_state.reason
-
end
-
end
-
timeout_value
-
end
-
end
-
-
# Behaves as {Future#result} but has one additional optional argument
-
# resolve_on_timeout.
-
#
-
# @!macro promises.resolvable.resolve_on_timeout
-
# @return [::Array(Boolean, Object, Exception), nil]
-
# @see Future#result
-
1
def result(timeout = nil, resolve_on_timeout = nil)
-
if wait_until_resolved timeout
-
internal_state.result
-
else
-
if resolve_on_timeout
-
unless resolve(*resolve_on_timeout, false)
-
# if it fails to resolve it was resolved in the meantime
-
# so return value as if there was no timeout
-
internal_state.result
-
end
-
end
-
# otherwise returns nil
-
end
-
end
-
-
# Creates new future wrapping receiver, effectively hiding the resolve method and similar.
-
#
-
# @return [Future]
-
1
def with_hidden_resolvable
-
@with_hidden_resolvable ||= FutureWrapperPromise.new_blocked_by1(self, @DefaultExecutor).future
-
end
-
end
-
-
# @abstract
-
# @private
-
1
class AbstractPromise < Synchronization::Object
-
1
safe_initialization!
-
1
include InternalStates
-
-
1
def initialize(future)
-
super()
-
@Future = future
-
end
-
-
1
def future
-
@Future
-
end
-
-
1
alias_method :event, :future
-
-
1
def default_executor
-
future.default_executor
-
end
-
-
1
def state
-
future.state
-
end
-
-
1
def touch
-
end
-
-
1
def to_s
-
format '%s %s>', super[0..-2], @Future
-
end
-
-
1
alias_method :inspect, :to_s
-
-
1
def delayed_because
-
nil
-
end
-
-
1
private
-
-
1
def resolve_with(new_state, raise_on_reassign = true)
-
@Future.resolve_with(new_state, raise_on_reassign)
-
end
-
-
# @return [Future]
-
1
def evaluate_to(*args, block)
-
resolve_with Fulfilled.new(block.call(*args))
-
rescue Exception => error
-
resolve_with Rejected.new(error)
-
raise error unless error.is_a?(StandardError)
-
end
-
end
-
-
1
class ResolvableEventPromise < AbstractPromise
-
1
def initialize(default_executor)
-
super ResolvableEvent.new(self, default_executor)
-
end
-
end
-
-
1
class ResolvableFuturePromise < AbstractPromise
-
1
def initialize(default_executor)
-
super ResolvableFuture.new(self, default_executor)
-
end
-
-
1
public :evaluate_to
-
end
-
-
# @abstract
-
1
class InnerPromise < AbstractPromise
-
end
-
-
# @abstract
-
1
class BlockedPromise < InnerPromise
-
-
1
private_class_method :new
-
-
1
def self.new_blocked_by1(blocker, *args, &block)
-
blocker_delayed = blocker.promise.delayed_because
-
promise = new(blocker_delayed, 1, *args, &block)
-
blocker.add_callback_notify_blocked promise, 0
-
promise
-
end
-
-
1
def self.new_blocked_by2(blocker1, blocker2, *args, &block)
-
blocker_delayed1 = blocker1.promise.delayed_because
-
blocker_delayed2 = blocker2.promise.delayed_because
-
delayed = if blocker_delayed1 && blocker_delayed2
-
# TODO (pitr-ch 23-Dec-2016): use arrays when we know it will not grow (only flat adds delay)
-
LockFreeStack.of2(blocker_delayed1, blocker_delayed2)
-
else
-
blocker_delayed1 || blocker_delayed2
-
end
-
promise = new(delayed, 2, *args, &block)
-
blocker1.add_callback_notify_blocked promise, 0
-
blocker2.add_callback_notify_blocked promise, 1
-
promise
-
end
-
-
1
def self.new_blocked_by(blockers, *args, &block)
-
delayed = blockers.reduce(nil) { |d, f| add_delayed d, f.promise.delayed_because }
-
promise = new(delayed, blockers.size, *args, &block)
-
blockers.each_with_index { |f, i| f.add_callback_notify_blocked promise, i }
-
promise
-
end
-
-
1
def self.add_delayed(delayed1, delayed2)
-
if delayed1 && delayed2
-
delayed1.push delayed2
-
delayed1
-
else
-
delayed1 || delayed2
-
end
-
end
-
-
1
def initialize(delayed, blockers_count, future)
-
super(future)
-
@Delayed = delayed
-
@Countdown = AtomicFixnum.new blockers_count
-
end
-
-
1
def on_blocker_resolution(future, index)
-
countdown = process_on_blocker_resolution(future, index)
-
resolvable = resolvable?(countdown, future, index)
-
-
on_resolvable(future, index) if resolvable
-
end
-
-
1
def delayed_because
-
@Delayed
-
end
-
-
1
def touch
-
clear_and_propagate_touch
-
end
-
-
# for inspection only
-
1
def blocked_by
-
blocked_by = []
-
ObjectSpace.each_object(AbstractEventFuture) { |o| blocked_by.push o if o.blocks.include? self }
-
blocked_by
-
end
-
-
1
private
-
-
1
def clear_and_propagate_touch(stack_or_element = @Delayed)
-
return if stack_or_element.nil?
-
-
if stack_or_element.is_a? LockFreeStack
-
stack_or_element.clear_each { |element| clear_and_propagate_touch element }
-
else
-
stack_or_element.touch unless stack_or_element.nil? # if still present
-
end
-
end
-
-
# @return [true,false] if resolvable
-
1
def resolvable?(countdown, future, index)
-
countdown.zero?
-
end
-
-
1
def process_on_blocker_resolution(future, index)
-
@Countdown.decrement
-
end
-
-
1
def on_resolvable(resolved_future, index)
-
raise NotImplementedError
-
end
-
end
-
-
# @abstract
-
1
class BlockedTaskPromise < BlockedPromise
-
1
def initialize(delayed, blockers_count, default_executor, executor, args, &task)
-
raise ArgumentError, 'no block given' unless block_given?
-
super delayed, 1, Future.new(self, default_executor)
-
@Executor = executor
-
@Task = task
-
@Args = args
-
end
-
-
1
def executor
-
@Executor
-
end
-
end
-
-
1
class ThenPromise < BlockedTaskPromise
-
1
private
-
-
1
def initialize(delayed, blockers_count, default_executor, executor, args, &task)
-
super delayed, blockers_count, default_executor, executor, args, &task
-
end
-
-
1
def on_resolvable(resolved_future, index)
-
if resolved_future.fulfilled?
-
Concurrent.executor(@Executor).post(resolved_future, @Args, @Task) do |future, args, task|
-
evaluate_to lambda { future.apply args, task }
-
end
-
else
-
resolve_with resolved_future.internal_state
-
end
-
end
-
end
-
-
1
class RescuePromise < BlockedTaskPromise
-
1
private
-
-
1
def initialize(delayed, blockers_count, default_executor, executor, args, &task)
-
super delayed, blockers_count, default_executor, executor, args, &task
-
end
-
-
1
def on_resolvable(resolved_future, index)
-
if resolved_future.rejected?
-
Concurrent.executor(@Executor).post(resolved_future, @Args, @Task) do |future, args, task|
-
evaluate_to lambda { future.apply args, task }
-
end
-
else
-
resolve_with resolved_future.internal_state
-
end
-
end
-
end
-
-
1
class ChainPromise < BlockedTaskPromise
-
1
private
-
-
1
def on_resolvable(resolved_future, index)
-
if Future === resolved_future
-
Concurrent.executor(@Executor).post(resolved_future, @Args, @Task) do |future, args, task|
-
evaluate_to(*future.result, *args, task)
-
end
-
else
-
Concurrent.executor(@Executor).post(@Args, @Task) do |args, task|
-
evaluate_to(*args, task)
-
end
-
end
-
end
-
end
-
-
# will be immediately resolved
-
1
class ImmediateEventPromise < InnerPromise
-
1
def initialize(default_executor)
-
super Event.new(self, default_executor).resolve_with(RESOLVED)
-
end
-
end
-
-
1
class ImmediateFuturePromise < InnerPromise
-
1
def initialize(default_executor, fulfilled, value, reason)
-
super Future.new(self, default_executor).
-
resolve_with(fulfilled ? Fulfilled.new(value) : Rejected.new(reason))
-
end
-
end
-
-
1
class AbstractFlatPromise < BlockedPromise
-
-
1
def initialize(delayed_because, blockers_count, event_or_future)
-
delayed = LockFreeStack.of1(self)
-
super(delayed, blockers_count, event_or_future)
-
# noinspection RubyArgCount
-
@Touched = AtomicBoolean.new false
-
@DelayedBecause = delayed_because || LockFreeStack.new
-
-
event_or_future.add_callback_clear_delayed_node delayed.peek
-
end
-
-
1
def touch
-
if @Touched.make_true
-
clear_and_propagate_touch @DelayedBecause
-
end
-
end
-
-
1
private
-
-
1
def touched?
-
@Touched.value
-
end
-
-
1
def on_resolvable(resolved_future, index)
-
resolve_with resolved_future.internal_state
-
end
-
-
1
def resolvable?(countdown, future, index)
-
!@Future.internal_state.resolved? && super(countdown, future, index)
-
end
-
-
1
def add_delayed_of(future)
-
delayed = future.promise.delayed_because
-
if touched?
-
clear_and_propagate_touch delayed
-
else
-
BlockedPromise.add_delayed @DelayedBecause, delayed
-
clear_and_propagate_touch @DelayedBecause if touched?
-
end
-
end
-
-
end
-
-
1
class FlatEventPromise < AbstractFlatPromise
-
-
1
private
-
-
1
def initialize(delayed, blockers_count, default_executor)
-
super delayed, 2, Event.new(self, default_executor)
-
end
-
-
1
def process_on_blocker_resolution(future, index)
-
countdown = super(future, index)
-
if countdown.nonzero?
-
internal_state = future.internal_state
-
-
unless internal_state.fulfilled?
-
resolve_with RESOLVED
-
return countdown
-
end
-
-
value = internal_state.value
-
case value
-
when AbstractEventFuture
-
add_delayed_of value
-
value.add_callback_notify_blocked self, nil
-
countdown
-
else
-
resolve_with RESOLVED
-
end
-
end
-
countdown
-
end
-
-
end
-
-
1
class FlatFuturePromise < AbstractFlatPromise
-
-
1
private
-
-
1
def initialize(delayed, blockers_count, levels, default_executor)
-
raise ArgumentError, 'levels has to be higher than 0' if levels < 1
-
# flat promise may result to a future having delayed futures, therefore we have to have empty stack
-
# to be able to add new delayed futures
-
super delayed || LockFreeStack.new, 1 + levels, Future.new(self, default_executor)
-
end
-
-
1
def process_on_blocker_resolution(future, index)
-
countdown = super(future, index)
-
if countdown.nonzero?
-
internal_state = future.internal_state
-
-
unless internal_state.fulfilled?
-
resolve_with internal_state
-
return countdown
-
end
-
-
value = internal_state.value
-
case value
-
when AbstractEventFuture
-
add_delayed_of value
-
value.add_callback_notify_blocked self, nil
-
countdown
-
else
-
evaluate_to(lambda { raise TypeError, "returned value #{value.inspect} is not a Future" })
-
end
-
end
-
countdown
-
end
-
-
end
-
-
1
class RunFuturePromise < AbstractFlatPromise
-
-
1
private
-
-
1
def initialize(delayed, blockers_count, default_executor, run_test)
-
super delayed, 1, Future.new(self, default_executor)
-
@RunTest = run_test
-
end
-
-
1
def process_on_blocker_resolution(future, index)
-
internal_state = future.internal_state
-
-
unless internal_state.fulfilled?
-
resolve_with internal_state
-
return 0
-
end
-
-
value = internal_state.value
-
continuation_future = @RunTest.call value
-
-
if continuation_future
-
add_delayed_of continuation_future
-
continuation_future.add_callback_notify_blocked self, nil
-
else
-
resolve_with internal_state
-
end
-
-
1
-
end
-
end
-
-
1
class ZipEventEventPromise < BlockedPromise
-
1
def initialize(delayed, blockers_count, default_executor)
-
super delayed, 2, Event.new(self, default_executor)
-
end
-
-
1
private
-
-
1
def on_resolvable(resolved_future, index)
-
resolve_with RESOLVED
-
end
-
end
-
-
1
class ZipFutureEventPromise < BlockedPromise
-
1
def initialize(delayed, blockers_count, default_executor)
-
super delayed, 2, Future.new(self, default_executor)
-
@result = nil
-
end
-
-
1
private
-
-
1
def process_on_blocker_resolution(future, index)
-
# first blocking is future, take its result
-
@result = future.internal_state if index == 0
-
# super has to be called after above to piggyback on volatile @Countdown
-
super future, index
-
end
-
-
1
def on_resolvable(resolved_future, index)
-
resolve_with @result
-
end
-
end
-
-
1
class EventWrapperPromise < BlockedPromise
-
1
def initialize(delayed, blockers_count, default_executor)
-
super delayed, 1, Event.new(self, default_executor)
-
end
-
-
1
private
-
-
1
def on_resolvable(resolved_future, index)
-
resolve_with RESOLVED
-
end
-
end
-
-
1
class FutureWrapperPromise < BlockedPromise
-
1
def initialize(delayed, blockers_count, default_executor)
-
super delayed, 1, Future.new(self, default_executor)
-
end
-
-
1
private
-
-
1
def on_resolvable(resolved_future, index)
-
resolve_with resolved_future.internal_state
-
end
-
end
-
-
1
class ZipFuturesPromise < BlockedPromise
-
-
1
private
-
-
1
def initialize(delayed, blockers_count, default_executor)
-
super(delayed, blockers_count, Future.new(self, default_executor))
-
@Resolutions = ::Array.new(blockers_count, nil)
-
-
on_resolvable nil, nil if blockers_count == 0
-
end
-
-
1
def process_on_blocker_resolution(future, index)
-
# TODO (pitr-ch 18-Dec-2016): Can we assume that array will never break under parallel access when never re-sized?
-
@Resolutions[index] = future.internal_state # has to be set before countdown in super
-
super future, index
-
end
-
-
1
def on_resolvable(resolved_future, index)
-
all_fulfilled = true
-
values = ::Array.new(@Resolutions.size)
-
reasons = ::Array.new(@Resolutions.size)
-
-
@Resolutions.each_with_index do |internal_state, i|
-
fulfilled, values[i], reasons[i] = internal_state.result
-
all_fulfilled &&= fulfilled
-
end
-
-
if all_fulfilled
-
resolve_with FulfilledArray.new(values)
-
else
-
resolve_with PartiallyRejected.new(values, reasons)
-
end
-
end
-
end
-
-
1
class ZipEventsPromise < BlockedPromise
-
-
1
private
-
-
1
def initialize(delayed, blockers_count, default_executor)
-
super delayed, blockers_count, Event.new(self, default_executor)
-
-
on_resolvable nil, nil if blockers_count == 0
-
end
-
-
1
def on_resolvable(resolved_future, index)
-
resolve_with RESOLVED
-
end
-
end
-
-
# @abstract
-
1
class AbstractAnyPromise < BlockedPromise
-
end
-
-
1
class AnyResolvedEventPromise < AbstractAnyPromise
-
-
1
private
-
-
1
def initialize(delayed, blockers_count, default_executor)
-
super delayed, blockers_count, Event.new(self, default_executor)
-
end
-
-
1
def resolvable?(countdown, future, index)
-
true
-
end
-
-
1
def on_resolvable(resolved_future, index)
-
resolve_with RESOLVED, false
-
end
-
end
-
-
1
class AnyResolvedFuturePromise < AbstractAnyPromise
-
-
1
private
-
-
1
def initialize(delayed, blockers_count, default_executor)
-
super delayed, blockers_count, Future.new(self, default_executor)
-
end
-
-
1
def resolvable?(countdown, future, index)
-
true
-
end
-
-
1
def on_resolvable(resolved_future, index)
-
resolve_with resolved_future.internal_state, false
-
end
-
end
-
-
1
class AnyFulfilledFuturePromise < AnyResolvedFuturePromise
-
-
1
private
-
-
1
def resolvable?(countdown, future, index)
-
future.fulfilled? ||
-
# inlined super from BlockedPromise
-
countdown.zero?
-
end
-
end
-
-
1
class DelayPromise < InnerPromise
-
-
1
def initialize(default_executor)
-
event = Event.new(self, default_executor)
-
@Delayed = LockFreeStack.of1(self)
-
super event
-
event.add_callback_clear_delayed_node @Delayed.peek
-
end
-
-
1
def touch
-
@Future.resolve_with RESOLVED
-
end
-
-
1
def delayed_because
-
@Delayed
-
end
-
-
end
-
-
1
class ScheduledPromise < InnerPromise
-
1
def intended_time
-
@IntendedTime
-
end
-
-
1
def inspect
-
"#{to_s[0..-2]} intended_time: #{@IntendedTime}>"
-
end
-
-
1
private
-
-
1
def initialize(default_executor, intended_time)
-
super Event.new(self, default_executor)
-
-
@IntendedTime = intended_time
-
-
in_seconds = begin
-
now = Time.now
-
schedule_time = if @IntendedTime.is_a? Time
-
@IntendedTime
-
else
-
now + @IntendedTime
-
end
-
[0, schedule_time.to_f - now.to_f].max
-
end
-
-
Concurrent.global_timer_set.post(in_seconds) do
-
@Future.resolve_with RESOLVED
-
end
-
end
-
end
-
-
1
extend FactoryMethods
-
-
1
private_constant :AbstractPromise,
-
:ResolvableEventPromise,
-
:ResolvableFuturePromise,
-
:InnerPromise,
-
:BlockedPromise,
-
:BlockedTaskPromise,
-
:ThenPromise,
-
:RescuePromise,
-
:ChainPromise,
-
:ImmediateEventPromise,
-
:ImmediateFuturePromise,
-
:AbstractFlatPromise,
-
:FlatFuturePromise,
-
:FlatEventPromise,
-
:RunFuturePromise,
-
:ZipEventEventPromise,
-
:ZipFutureEventPromise,
-
:EventWrapperPromise,
-
:FutureWrapperPromise,
-
:ZipFuturesPromise,
-
:ZipEventsPromise,
-
:AbstractAnyPromise,
-
:AnyResolvedFuturePromise,
-
:AnyFulfilledFuturePromise,
-
:AnyResolvedEventPromise,
-
:DelayPromise,
-
:ScheduledPromise
-
-
-
end
-
end
-
1
module Concurrent
-
-
# Methods form module A included to a module B, which is already included into class C,
-
# will not be visible in the C class. If this module is extended to B then A's methods
-
# are correctly made visible to C.
-
#
-
# @example
-
# module A
-
# def a
-
# :a
-
# end
-
# end
-
#
-
# module B1
-
# end
-
#
-
# class C1
-
# include B1
-
# end
-
#
-
# module B2
-
# extend Concurrent::ReInclude
-
# end
-
#
-
# class C2
-
# include B2
-
# end
-
#
-
# B1.send :include, A
-
# B2.send :include, A
-
#
-
# C1.new.respond_to? :a # => false
-
# C2.new.respond_to? :a # => true
-
1
module ReInclude
-
# @!visibility private
-
1
def included(base)
-
(@re_include_to_bases ||= []) << [:include, base]
-
super(base)
-
end
-
-
# @!visibility private
-
1
def extended(base)
-
2
(@re_include_to_bases ||= []) << [:extend, base]
-
2
super(base)
-
end
-
-
# @!visibility private
-
1
def include(*modules)
-
1
result = super(*modules)
-
1
modules.reverse.each do |module_being_included|
-
1
(@re_include_to_bases ||= []).each do |method, mod|
-
1
mod.send method, module_being_included
-
end
-
end
-
1
result
-
end
-
end
-
end
-
1
require 'concurrent/constants'
-
1
require 'concurrent/errors'
-
1
require 'concurrent/configuration'
-
1
require 'concurrent/ivar'
-
1
require 'concurrent/collection/copy_on_notify_observer_set'
-
1
require 'concurrent/utility/monotonic_time'
-
-
1
require 'concurrent/options'
-
-
1
module Concurrent
-
-
# `ScheduledTask` is a close relative of `Concurrent::Future` but with one
-
# important difference: A `Future` is set to execute as soon as possible
-
# whereas a `ScheduledTask` is set to execute after a specified delay. This
-
# implementation is loosely based on Java's
-
# [ScheduledExecutorService](http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/ScheduledExecutorService.html).
-
# It is a more feature-rich variant of {Concurrent.timer}.
-
#
-
# The *intended* schedule time of task execution is set on object construction
-
# with the `delay` argument. The delay is a numeric (floating point or integer)
-
# representing a number of seconds in the future. Any other value or a numeric
-
# equal to or less than zero will result in an exception. The *actual* schedule
-
# time of task execution is set when the `execute` method is called.
-
#
-
# The constructor can also be given zero or more processing options. Currently
-
# the only supported options are those recognized by the
-
# [Dereferenceable](Dereferenceable) module.
-
#
-
# The final constructor argument is a block representing the task to be performed.
-
# If no block is given an `ArgumentError` will be raised.
-
#
-
# **States**
-
#
-
# `ScheduledTask` mixes in the [Obligation](Obligation) module thus giving it
-
# "future" behavior. This includes the expected lifecycle states. `ScheduledTask`
-
# has one additional state, however. While the task (block) is being executed the
-
# state of the object will be `:processing`. This additional state is necessary
-
# because it has implications for task cancellation.
-
#
-
# **Cancellation**
-
#
-
# A `:pending` task can be cancelled using the `#cancel` method. A task in any
-
# other state, including `:processing`, cannot be cancelled. The `#cancel`
-
# method returns a boolean indicating the success of the cancellation attempt.
-
# A cancelled `ScheduledTask` cannot be restarted. It is immutable.
-
#
-
# **Obligation and Observation**
-
#
-
# The result of a `ScheduledTask` can be obtained either synchronously or
-
# asynchronously. `ScheduledTask` mixes in both the [Obligation](Obligation)
-
# module and the
-
# [Observable](http://ruby-doc.org/stdlib-2.0/libdoc/observer/rdoc/Observable.html)
-
# module from the Ruby standard library. With one exception `ScheduledTask`
-
# behaves identically to [Future](Observable) with regard to these modules.
-
#
-
# @!macro copy_options
-
#
-
# @example Basic usage
-
#
-
# require 'concurrent'
-
# require 'thread' # for Queue
-
# require 'open-uri' # for open(uri)
-
#
-
# class Ticker
-
# def get_year_end_closing(symbol, year)
-
# uri = "http://ichart.finance.yahoo.com/table.csv?s=#{symbol}&a=11&b=01&c=#{year}&d=11&e=31&f=#{year}&g=m"
-
# data = open(uri) {|f| f.collect{|line| line.strip } }
-
# data[1].split(',')[4].to_f
-
# end
-
# end
-
#
-
# # Future
-
# price = Concurrent::Future.execute{ Ticker.new.get_year_end_closing('TWTR', 2013) }
-
# price.state #=> :pending
-
# sleep(1) # do other stuff
-
# price.value #=> 63.65
-
# price.state #=> :fulfilled
-
#
-
# # ScheduledTask
-
# task = Concurrent::ScheduledTask.execute(2){ Ticker.new.get_year_end_closing('INTC', 2013) }
-
# task.state #=> :pending
-
# sleep(3) # do other stuff
-
# task.value #=> 25.96
-
#
-
# @example Successful task execution
-
#
-
# task = Concurrent::ScheduledTask.new(2){ 'What does the fox say?' }
-
# task.state #=> :unscheduled
-
# task.execute
-
# task.state #=> pending
-
#
-
# # wait for it...
-
# sleep(3)
-
#
-
# task.unscheduled? #=> false
-
# task.pending? #=> false
-
# task.fulfilled? #=> true
-
# task.rejected? #=> false
-
# task.value #=> 'What does the fox say?'
-
#
-
# @example One line creation and execution
-
#
-
# task = Concurrent::ScheduledTask.new(2){ 'What does the fox say?' }.execute
-
# task.state #=> pending
-
#
-
# task = Concurrent::ScheduledTask.execute(2){ 'What do you get when you multiply 6 by 9?' }
-
# task.state #=> pending
-
#
-
# @example Failed task execution
-
#
-
# task = Concurrent::ScheduledTask.execute(2){ raise StandardError.new('Call me maybe?') }
-
# task.pending? #=> true
-
#
-
# # wait for it...
-
# sleep(3)
-
#
-
# task.unscheduled? #=> false
-
# task.pending? #=> false
-
# task.fulfilled? #=> false
-
# task.rejected? #=> true
-
# task.value #=> nil
-
# task.reason #=> #<StandardError: Call me maybe?>
-
#
-
# @example Task execution with observation
-
#
-
# observer = Class.new{
-
# def update(time, value, reason)
-
# puts "The task completed at #{time} with value '#{value}'"
-
# end
-
# }.new
-
#
-
# task = Concurrent::ScheduledTask.new(2){ 'What does the fox say?' }
-
# task.add_observer(observer)
-
# task.execute
-
# task.pending? #=> true
-
#
-
# # wait for it...
-
# sleep(3)
-
#
-
# #>> The task completed at 2013-11-07 12:26:09 -0500 with value 'What does the fox say?'
-
#
-
# @!macro monotonic_clock_warning
-
#
-
# @see Concurrent.timer
-
1
class ScheduledTask < IVar
-
1
include Comparable
-
-
# The executor on which to execute the task.
-
# @!visibility private
-
1
attr_reader :executor
-
-
# Schedule a task for execution at a specified future time.
-
#
-
# @param [Float] delay the number of seconds to wait for before executing the task
-
#
-
# @yield the task to be performed
-
#
-
# @!macro executor_and_deref_options
-
#
-
# @option opts [object, Array] :args zero or more arguments to be passed the task
-
# block on execution
-
#
-
# @raise [ArgumentError] When no block is given
-
# @raise [ArgumentError] When given a time that is in the past
-
1
def initialize(delay, opts = {}, &task)
-
raise ArgumentError.new('no block given') unless block_given?
-
raise ArgumentError.new('seconds must be greater than zero') if delay.to_f < 0.0
-
-
super(NULL, opts, &nil)
-
-
synchronize do
-
ns_set_state(:unscheduled)
-
@parent = opts.fetch(:timer_set, Concurrent.global_timer_set)
-
@args = get_arguments_from(opts)
-
@delay = delay.to_f
-
@task = task
-
@time = nil
-
@executor = Options.executor_from_options(opts) || Concurrent.global_io_executor
-
self.observers = Collection::CopyOnNotifyObserverSet.new
-
end
-
end
-
-
# The `delay` value given at instanciation.
-
#
-
# @return [Float] the initial delay.
-
1
def initial_delay
-
synchronize { @delay }
-
end
-
-
# The monotonic time at which the the task is scheduled to be executed.
-
#
-
# @return [Float] the schedule time or nil if `unscheduled`
-
1
def schedule_time
-
synchronize { @time }
-
end
-
-
# Comparator which orders by schedule time.
-
#
-
# @!visibility private
-
1
def <=>(other)
-
schedule_time <=> other.schedule_time
-
end
-
-
# Has the task been cancelled?
-
#
-
# @return [Boolean] true if the task is in the given state else false
-
1
def cancelled?
-
synchronize { ns_check_state?(:cancelled) }
-
end
-
-
# In the task execution in progress?
-
#
-
# @return [Boolean] true if the task is in the given state else false
-
1
def processing?
-
synchronize { ns_check_state?(:processing) }
-
end
-
-
# Cancel this task and prevent it from executing. A task can only be
-
# cancelled if it is pending or unscheduled.
-
#
-
# @return [Boolean] true if successfully cancelled else false
-
1
def cancel
-
if compare_and_set_state(:cancelled, :pending, :unscheduled)
-
complete(false, nil, CancelledOperationError.new)
-
# To avoid deadlocks this call must occur outside of #synchronize
-
# Changing the state above should prevent redundant calls
-
@parent.send(:remove_task, self)
-
else
-
false
-
end
-
end
-
-
# Reschedule the task using the original delay and the current time.
-
# A task can only be reset while it is `:pending`.
-
#
-
# @return [Boolean] true if successfully rescheduled else false
-
1
def reset
-
synchronize{ ns_reschedule(@delay) }
-
end
-
-
# Reschedule the task using the given delay and the current time.
-
# A task can only be reset while it is `:pending`.
-
#
-
# @param [Float] delay the number of seconds to wait for before executing the task
-
#
-
# @return [Boolean] true if successfully rescheduled else false
-
#
-
# @raise [ArgumentError] When given a time that is in the past
-
1
def reschedule(delay)
-
delay = delay.to_f
-
raise ArgumentError.new('seconds must be greater than zero') if delay < 0.0
-
synchronize{ ns_reschedule(delay) }
-
end
-
-
# Execute an `:unscheduled` `ScheduledTask`. Immediately sets the state to `:pending`
-
# and starts counting down toward execution. Does nothing if the `ScheduledTask` is
-
# in any state other than `:unscheduled`.
-
#
-
# @return [ScheduledTask] a reference to `self`
-
1
def execute
-
if compare_and_set_state(:pending, :unscheduled)
-
synchronize{ ns_schedule(@delay) }
-
end
-
self
-
end
-
-
# Create a new `ScheduledTask` object with the given block, execute it, and return the
-
# `:pending` object.
-
#
-
# @param [Float] delay the number of seconds to wait for before executing the task
-
#
-
# @!macro executor_and_deref_options
-
#
-
# @return [ScheduledTask] the newly created `ScheduledTask` in the `:pending` state
-
#
-
# @raise [ArgumentError] if no block is given
-
1
def self.execute(delay, opts = {}, &task)
-
new(delay, opts, &task).execute
-
end
-
-
# Execute the task.
-
#
-
# @!visibility private
-
1
def process_task
-
safe_execute(@task, @args)
-
end
-
-
1
protected :set, :try_set, :fail, :complete
-
-
1
protected
-
-
# Schedule the task using the given delay and the current time.
-
#
-
# @param [Float] delay the number of seconds to wait for before executing the task
-
#
-
# @return [Boolean] true if successfully rescheduled else false
-
#
-
# @!visibility private
-
1
def ns_schedule(delay)
-
@delay = delay
-
@time = Concurrent.monotonic_time + @delay
-
@parent.send(:post_task, self)
-
end
-
-
# Reschedule the task using the given delay and the current time.
-
# A task can only be reset while it is `:pending`.
-
#
-
# @param [Float] delay the number of seconds to wait for before executing the task
-
#
-
# @return [Boolean] true if successfully rescheduled else false
-
#
-
# @!visibility private
-
1
def ns_reschedule(delay)
-
return false unless ns_check_state?(:pending)
-
@parent.send(:remove_task, self) && ns_schedule(delay)
-
end
-
end
-
end
-
1
require 'concurrent/utility/engine'
-
1
require 'concurrent/thread_safe/util'
-
1
require 'set'
-
-
1
module Concurrent
-
-
# @!macro concurrent_set
-
#
-
# A thread-safe subclass of Set. This version locks against the object
-
# itself for every method call, ensuring only one thread can be reading
-
# or writing at a time. This includes iteration methods like `#each`.
-
#
-
# @note `a += b` is **not** a **thread-safe** operation on
-
# `Concurrent::Set`. It reads Set `a`, then it creates new `Concurrent::Set`
-
# which is union of `a` and `b`, then it writes the union to `a`.
-
# The read and write are independent operations they do not form a single atomic
-
# operation therefore when two `+=` operations are executed concurrently updates
-
# may be lost. Use `#merge` instead.
-
#
-
# @see http://ruby-doc.org/stdlib-2.4.0/libdoc/set/rdoc/Set.html Ruby standard library `Set`
-
-
-
# @!macro internal_implementation_note
-
SetImplementation = case
-
1
when Concurrent.on_cruby?
-
# Because MRI never runs code in parallel, the existing
-
# non-thread-safe structures should usually work fine.
-
1
::Set
-
-
when Concurrent.on_jruby?
-
require 'jruby/synchronized'
-
-
class JRubySet < ::Set
-
include JRuby::Synchronized
-
end
-
JRubySet
-
-
when Concurrent.on_rbx?
-
require 'monitor'
-
require 'concurrent/thread_safe/util/data_structures'
-
-
class RbxSet < ::Set
-
end
-
ThreadSafe::Util.make_synchronized_on_rbx Concurrent::RbxSet
-
RbxSet
-
-
when Concurrent.on_truffleruby?
-
require 'concurrent/thread_safe/util/data_structures'
-
-
class TruffleRubySet < ::Set
-
end
-
-
ThreadSafe::Util.make_synchronized_on_truffleruby Concurrent::TruffleRubySet
-
TruffleRubySet
-
-
else
-
warn 'Possibly unsupported Ruby implementation'
-
::Set
-
end
-
1
private_constant :SetImplementation
-
-
# @!macro concurrent_set
-
1
class Set < SetImplementation
-
end
-
end
-
-
1
require 'concurrent/synchronization/abstract_struct'
-
1
require 'concurrent/errors'
-
1
require 'concurrent/synchronization'
-
-
1
module Concurrent
-
-
# An thread-safe, write-once variation of Ruby's standard `Struct`.
-
# Each member can have its value set at most once, either at construction
-
# or any time thereafter. Attempting to assign a value to a member
-
# that has already been set will result in a `Concurrent::ImmutabilityError`.
-
#
-
# @see http://ruby-doc.org/core-2.2.0/Struct.html Ruby standard library `Struct`
-
# @see http://en.wikipedia.org/wiki/Final_(Java) Java `final` keyword
-
1
module SettableStruct
-
1
include Synchronization::AbstractStruct
-
-
# @!macro struct_values
-
1
def values
-
synchronize { ns_values }
-
end
-
1
alias_method :to_a, :values
-
-
# @!macro struct_values_at
-
1
def values_at(*indexes)
-
synchronize { ns_values_at(indexes) }
-
end
-
-
# @!macro struct_inspect
-
1
def inspect
-
synchronize { ns_inspect }
-
end
-
1
alias_method :to_s, :inspect
-
-
# @!macro struct_merge
-
1
def merge(other, &block)
-
synchronize { ns_merge(other, &block) }
-
end
-
-
# @!macro struct_to_h
-
1
def to_h
-
synchronize { ns_to_h }
-
end
-
-
# @!macro struct_get
-
1
def [](member)
-
synchronize { ns_get(member) }
-
end
-
-
# @!macro struct_equality
-
1
def ==(other)
-
synchronize { ns_equality(other) }
-
end
-
-
# @!macro struct_each
-
1
def each(&block)
-
return enum_for(:each) unless block_given?
-
synchronize { ns_each(&block) }
-
end
-
-
# @!macro struct_each_pair
-
1
def each_pair(&block)
-
return enum_for(:each_pair) unless block_given?
-
synchronize { ns_each_pair(&block) }
-
end
-
-
# @!macro struct_select
-
1
def select(&block)
-
return enum_for(:select) unless block_given?
-
synchronize { ns_select(&block) }
-
end
-
-
# @!macro struct_set
-
#
-
# @raise [Concurrent::ImmutabilityError] if the given member has already been set
-
1
def []=(member, value)
-
if member.is_a? Integer
-
length = synchronize { @values.length }
-
if member >= length
-
raise IndexError.new("offset #{member} too large for struct(size:#{length})")
-
end
-
synchronize do
-
unless @values[member].nil?
-
raise Concurrent::ImmutabilityError.new('struct member has already been set')
-
end
-
@values[member] = value
-
end
-
else
-
send("#{member}=", value)
-
end
-
rescue NoMethodError
-
raise NameError.new("no member '#{member}' in struct")
-
end
-
-
1
private
-
-
# @!visibility private
-
1
def initialize_copy(original)
-
synchronize do
-
super(original)
-
ns_initialize_copy
-
end
-
end
-
-
# @!macro struct_new
-
1
def self.new(*args, &block)
-
clazz_name = nil
-
if args.length == 0
-
raise ArgumentError.new('wrong number of arguments (0 for 1+)')
-
elsif args.length > 0 && args.first.is_a?(String)
-
clazz_name = args.shift
-
end
-
FACTORY.define_struct(clazz_name, args, &block)
-
end
-
-
1
FACTORY = Class.new(Synchronization::LockableObject) do
-
1
def define_struct(name, members, &block)
-
synchronize do
-
clazz = Synchronization::AbstractStruct.define_struct_class(SettableStruct, Synchronization::LockableObject, name, members, &block)
-
members.each_with_index do |member, index|
-
clazz.send :remove_method, member if clazz.instance_methods.include? member
-
clazz.send(:define_method, member) do
-
synchronize { @values[index] }
-
end
-
clazz.send(:define_method, "#{member}=") do |value|
-
synchronize do
-
unless @values[index].nil?
-
raise Concurrent::ImmutabilityError.new('struct member has already been set')
-
end
-
@values[index] = value
-
end
-
end
-
end
-
clazz
-
end
-
end
-
end.new
-
1
private_constant :FACTORY
-
end
-
end
-
1
require 'concurrent/utility/engine'
-
-
1
require 'concurrent/synchronization/abstract_object'
-
1
require 'concurrent/utility/native_extension_loader' # load native parts first
-
1
Concurrent.load_native_extensions
-
-
1
require 'concurrent/synchronization/mri_object'
-
1
require 'concurrent/synchronization/jruby_object'
-
1
require 'concurrent/synchronization/rbx_object'
-
1
require 'concurrent/synchronization/truffleruby_object'
-
1
require 'concurrent/synchronization/object'
-
1
require 'concurrent/synchronization/volatile'
-
-
1
require 'concurrent/synchronization/abstract_lockable_object'
-
1
require 'concurrent/synchronization/mutex_lockable_object'
-
1
require 'concurrent/synchronization/jruby_lockable_object'
-
1
require 'concurrent/synchronization/rbx_lockable_object'
-
-
1
require 'concurrent/synchronization/lockable_object'
-
-
1
require 'concurrent/synchronization/condition'
-
1
require 'concurrent/synchronization/lock'
-
-
1
module Concurrent
-
# {include:file:docs-source/synchronization.md}
-
# {include:file:docs-source/synchronization-notes.md}
-
1
module Synchronization
-
end
-
end
-
-
1
module Concurrent
-
1
module Synchronization
-
-
# @!visibility private
-
1
class AbstractLockableObject < Synchronization::Object
-
-
1
protected
-
-
# @!macro synchronization_object_method_synchronize
-
#
-
# @yield runs the block synchronized against this object,
-
# equivalent of java's `synchronize(this) {}`
-
# @note can by made public in descendants if required by `public :synchronize`
-
1
def synchronize
-
raise NotImplementedError
-
end
-
-
# @!macro synchronization_object_method_ns_wait_until
-
#
-
# Wait until condition is met or timeout passes,
-
# protects against spurious wake-ups.
-
# @param [Numeric, nil] timeout in seconds, `nil` means no timeout
-
# @yield condition to be met
-
# @yieldreturn [true, false]
-
# @return [true, false] if condition met
-
# @note only to be used inside synchronized block
-
# @note to provide direct access to this method in a descendant add method
-
# ```
-
# def wait_until(timeout = nil, &condition)
-
# synchronize { ns_wait_until(timeout, &condition) }
-
# end
-
# ```
-
1
def ns_wait_until(timeout = nil, &condition)
-
if timeout
-
wait_until = Concurrent.monotonic_time + timeout
-
loop do
-
now = Concurrent.monotonic_time
-
condition_result = condition.call
-
return condition_result if now >= wait_until || condition_result
-
ns_wait wait_until - now
-
end
-
else
-
ns_wait timeout until condition.call
-
true
-
end
-
end
-
-
# @!macro synchronization_object_method_ns_wait
-
#
-
# Wait until another thread calls #signal or #broadcast,
-
# spurious wake-ups can happen.
-
#
-
# @param [Numeric, nil] timeout in seconds, `nil` means no timeout
-
# @return [self]
-
# @note only to be used inside synchronized block
-
# @note to provide direct access to this method in a descendant add method
-
# ```
-
# def wait(timeout = nil)
-
# synchronize { ns_wait(timeout) }
-
# end
-
# ```
-
1
def ns_wait(timeout = nil)
-
raise NotImplementedError
-
end
-
-
# @!macro synchronization_object_method_ns_signal
-
#
-
# Signal one waiting thread.
-
# @return [self]
-
# @note only to be used inside synchronized block
-
# @note to provide direct access to this method in a descendant add method
-
# ```
-
# def signal
-
# synchronize { ns_signal }
-
# end
-
# ```
-
1
def ns_signal
-
raise NotImplementedError
-
end
-
-
# @!macro synchronization_object_method_ns_broadcast
-
#
-
# Broadcast to all waiting threads.
-
# @return [self]
-
# @note only to be used inside synchronized block
-
# @note to provide direct access to this method in a descendant add method
-
# ```
-
# def broadcast
-
# synchronize { ns_broadcast }
-
# end
-
# ```
-
1
def ns_broadcast
-
raise NotImplementedError
-
end
-
-
end
-
end
-
end
-
1
module Concurrent
-
1
module Synchronization
-
-
# @!visibility private
-
# @!macro internal_implementation_note
-
1
class AbstractObject
-
-
# @abstract has to be implemented based on Ruby runtime
-
1
def initialize
-
raise NotImplementedError
-
end
-
-
# @!visibility private
-
# @abstract
-
1
def full_memory_barrier
-
raise NotImplementedError
-
end
-
-
1
def self.attr_volatile(*names)
-
raise NotImplementedError
-
end
-
end
-
end
-
end
-
1
module Concurrent
-
1
module Synchronization
-
-
# @!visibility private
-
# @!macro internal_implementation_note
-
1
module AbstractStruct
-
-
# @!visibility private
-
1
def initialize(*values)
-
super()
-
ns_initialize(*values)
-
end
-
-
# @!macro struct_length
-
#
-
# Returns the number of struct members.
-
#
-
# @return [Fixnum] the number of struct members
-
1
def length
-
self.class::MEMBERS.length
-
end
-
1
alias_method :size, :length
-
-
# @!macro struct_members
-
#
-
# Returns the struct members as an array of symbols.
-
#
-
# @return [Array] the struct members as an array of symbols
-
1
def members
-
self.class::MEMBERS.dup
-
end
-
-
1
protected
-
-
# @!macro struct_values
-
#
-
# @!visibility private
-
1
def ns_values
-
@values.dup
-
end
-
-
# @!macro struct_values_at
-
#
-
# @!visibility private
-
1
def ns_values_at(indexes)
-
@values.values_at(*indexes)
-
end
-
-
# @!macro struct_to_h
-
#
-
# @!visibility private
-
1
def ns_to_h
-
length.times.reduce({}){|memo, i| memo[self.class::MEMBERS[i]] = @values[i]; memo}
-
end
-
-
# @!macro struct_get
-
#
-
# @!visibility private
-
1
def ns_get(member)
-
if member.is_a? Integer
-
if member >= @values.length
-
raise IndexError.new("offset #{member} too large for struct(size:#{@values.length})")
-
end
-
@values[member]
-
else
-
send(member)
-
end
-
rescue NoMethodError
-
raise NameError.new("no member '#{member}' in struct")
-
end
-
-
# @!macro struct_equality
-
#
-
# @!visibility private
-
1
def ns_equality(other)
-
self.class == other.class && self.values == other.values
-
end
-
-
# @!macro struct_each
-
#
-
# @!visibility private
-
1
def ns_each
-
values.each{|value| yield value }
-
end
-
-
# @!macro struct_each_pair
-
#
-
# @!visibility private
-
1
def ns_each_pair
-
@values.length.times do |index|
-
yield self.class::MEMBERS[index], @values[index]
-
end
-
end
-
-
# @!macro struct_select
-
#
-
# @!visibility private
-
1
def ns_select
-
values.select{|value| yield value }
-
end
-
-
# @!macro struct_inspect
-
#
-
# @!visibility private
-
1
def ns_inspect
-
struct = pr_underscore(self.class.ancestors[1])
-
clazz = ((self.class.to_s =~ /^#<Class:/) == 0) ? '' : " #{self.class}"
-
"#<#{struct}#{clazz} #{ns_to_h}>"
-
end
-
-
# @!macro struct_merge
-
#
-
# @!visibility private
-
1
def ns_merge(other, &block)
-
self.class.new(*self.to_h.merge(other, &block).values)
-
end
-
-
# @!visibility private
-
1
def ns_initialize_copy
-
@values = @values.map do |val|
-
begin
-
val.clone
-
rescue TypeError
-
val
-
end
-
end
-
end
-
-
# @!visibility private
-
1
def pr_underscore(clazz)
-
word = clazz.to_s.dup # dup string to workaround JRuby 9.2.0.0 bug https://github.com/jruby/jruby/issues/5229
-
word.gsub!(/::/, '/')
-
word.gsub!(/([A-Z]+)([A-Z][a-z])/,'\1_\2')
-
word.gsub!(/([a-z\d])([A-Z])/,'\1_\2')
-
word.tr!("-", "_")
-
word.downcase!
-
word
-
end
-
-
# @!visibility private
-
1
def self.define_struct_class(parent, base, name, members, &block)
-
clazz = Class.new(base || Object) do
-
include parent
-
self.const_set(:MEMBERS, members.collect{|member| member.to_s.to_sym}.freeze)
-
def ns_initialize(*values)
-
raise ArgumentError.new('struct size differs') if values.length > length
-
@values = values.fill(nil, values.length..length-1)
-
end
-
end
-
unless name.nil?
-
begin
-
parent.send :remove_const, name if parent.const_defined?(name, false)
-
parent.const_set(name, clazz)
-
clazz
-
rescue NameError
-
raise NameError.new("identifier #{name} needs to be constant")
-
end
-
end
-
members.each_with_index do |member, index|
-
clazz.send :remove_method, member if clazz.instance_methods.include? member
-
clazz.send(:define_method, member) do
-
@values[index]
-
end
-
end
-
clazz.class_exec(&block) unless block.nil?
-
clazz.singleton_class.send :alias_method, :[], :new
-
clazz
-
end
-
end
-
end
-
end
-
1
module Concurrent
-
1
module Synchronization
-
-
# @!visibility private
-
# TODO (pitr-ch 04-Dec-2016): should be in edge
-
1
class Condition < LockableObject
-
1
safe_initialization!
-
-
# TODO (pitr 12-Sep-2015): locks two objects, improve
-
# TODO (pitr 26-Sep-2015): study
-
# http://grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/8-b132/java/util/concurrent/locks/AbstractQueuedSynchronizer.java#AbstractQueuedSynchronizer.Node
-
-
1
singleton_class.send :alias_method, :private_new, :new
-
1
private_class_method :new
-
-
1
def initialize(lock)
-
super()
-
@Lock = lock
-
end
-
-
1
def wait(timeout = nil)
-
@Lock.synchronize { ns_wait(timeout) }
-
end
-
-
1
def ns_wait(timeout = nil)
-
synchronize { super(timeout) }
-
end
-
-
1
def wait_until(timeout = nil, &condition)
-
@Lock.synchronize { ns_wait_until(timeout, &condition) }
-
end
-
-
1
def ns_wait_until(timeout = nil, &condition)
-
synchronize { super(timeout, &condition) }
-
end
-
-
1
def signal
-
@Lock.synchronize { ns_signal }
-
end
-
-
1
def ns_signal
-
synchronize { super }
-
end
-
-
1
def broadcast
-
@Lock.synchronize { ns_broadcast }
-
end
-
-
1
def ns_broadcast
-
synchronize { super }
-
end
-
end
-
-
1
class LockableObject < LockableObjectImplementation
-
1
def new_condition
-
Condition.private_new(self)
-
end
-
end
-
end
-
end
-
1
module Concurrent
-
1
module Synchronization
-
-
1
if Concurrent.on_jruby? && Concurrent.java_extensions_loaded?
-
-
# @!visibility private
-
# @!macro internal_implementation_note
-
class JRubyLockableObject < AbstractLockableObject
-
-
end
-
end
-
end
-
end
-
1
module Concurrent
-
1
module Synchronization
-
-
1
if Concurrent.on_jruby? && Concurrent.java_extensions_loaded?
-
-
# @!visibility private
-
module JRubyAttrVolatile
-
def self.included(base)
-
base.extend(ClassMethods)
-
end
-
-
module ClassMethods
-
def attr_volatile(*names)
-
names.each do |name|
-
-
ivar = :"@volatile_#{name}"
-
-
class_eval <<-RUBY, __FILE__, __LINE__ + 1
-
def #{name}
-
instance_variable_get_volatile(:#{ivar})
-
end
-
-
def #{name}=(value)
-
instance_variable_set_volatile(:#{ivar}, value)
-
end
-
RUBY
-
-
end
-
names.map { |n| [n, :"#{n}="] }.flatten
-
end
-
end
-
end
-
-
# @!visibility private
-
# @!macro internal_implementation_note
-
class JRubyObject < AbstractObject
-
include JRubyAttrVolatile
-
-
def initialize
-
# nothing to do
-
end
-
end
-
end
-
end
-
end
-
1
module Concurrent
-
1
module Synchronization
-
-
# @!visibility private
-
# TODO (pitr-ch 04-Dec-2016): should be in edge
-
1
class Lock < LockableObject
-
# TODO use JavaReentrantLock on JRuby
-
-
1
public :synchronize
-
-
1
def wait(timeout = nil)
-
synchronize { ns_wait(timeout) }
-
end
-
-
1
public :ns_wait
-
-
1
def wait_until(timeout = nil, &condition)
-
synchronize { ns_wait_until(timeout, &condition) }
-
end
-
-
1
public :ns_wait_until
-
-
1
def signal
-
synchronize { ns_signal }
-
end
-
-
1
public :ns_signal
-
-
1
def broadcast
-
synchronize { ns_broadcast }
-
end
-
-
1
public :ns_broadcast
-
end
-
end
-
end
-
1
module Concurrent
-
1
module Synchronization
-
-
# @!visibility private
-
# @!macro internal_implementation_note
-
LockableObjectImplementation = case
-
1
when Concurrent.on_cruby? && Concurrent.ruby_version(:<=, 1, 9, 3)
-
MonitorLockableObject
-
when Concurrent.on_cruby? && Concurrent.ruby_version(:>, 1, 9, 3)
-
1
MutexLockableObject
-
when Concurrent.on_jruby?
-
JRubyLockableObject
-
when Concurrent.on_rbx?
-
RbxLockableObject
-
when Concurrent.on_truffleruby?
-
MutexLockableObject
-
else
-
warn 'Possibly unsupported Ruby implementation'
-
MonitorLockableObject
-
end
-
1
private_constant :LockableObjectImplementation
-
-
# Safe synchronization under any Ruby implementation.
-
# It provides methods like {#synchronize}, {#wait}, {#signal} and {#broadcast}.
-
# Provides a single layer which can improve its implementation over time without changes needed to
-
# the classes using it. Use {Synchronization::Object} not this abstract class.
-
#
-
# @note this object does not support usage together with
-
# [`Thread#wakeup`](http://ruby-doc.org/core-2.2.0/Thread.html#method-i-wakeup)
-
# and [`Thread#raise`](http://ruby-doc.org/core-2.2.0/Thread.html#method-i-raise).
-
# `Thread#sleep` and `Thread#wakeup` will work as expected but mixing `Synchronization::Object#wait` and
-
# `Thread#wakeup` will not work on all platforms.
-
#
-
# @see Event implementation as an example of this class use
-
#
-
# @example simple
-
# class AnClass < Synchronization::Object
-
# def initialize
-
# super
-
# synchronize { @value = 'asd' }
-
# end
-
#
-
# def value
-
# synchronize { @value }
-
# end
-
# end
-
#
-
# @!visibility private
-
1
class LockableObject < LockableObjectImplementation
-
-
# TODO (pitr 12-Sep-2015): make private for c-r, prohibit subclassing
-
# TODO (pitr 12-Sep-2015): we inherit too much ourselves :/
-
-
# @!method initialize(*args, &block)
-
# @!macro synchronization_object_method_initialize
-
-
# @!method synchronize
-
# @!macro synchronization_object_method_synchronize
-
-
# @!method wait_until(timeout = nil, &condition)
-
# @!macro synchronization_object_method_ns_wait_until
-
-
# @!method wait(timeout = nil)
-
# @!macro synchronization_object_method_ns_wait
-
-
# @!method signal
-
# @!macro synchronization_object_method_ns_signal
-
-
# @!method broadcast
-
# @!macro synchronization_object_method_ns_broadcast
-
-
end
-
end
-
end
-
1
module Concurrent
-
1
module Synchronization
-
-
# @!visibility private
-
1
module MriAttrVolatile
-
1
def self.included(base)
-
1
base.extend(ClassMethods)
-
end
-
-
1
module ClassMethods
-
1
def attr_volatile(*names)
-
names.each do |name|
-
ivar = :"@volatile_#{name}"
-
class_eval <<-RUBY, __FILE__, __LINE__ + 1
-
def #{name}
-
#{ivar}
-
end
-
-
def #{name}=(value)
-
#{ivar} = value
-
end
-
RUBY
-
end
-
names.map { |n| [n, :"#{n}="] }.flatten
-
end
-
end
-
-
1
def full_memory_barrier
-
# relying on undocumented behavior of CRuby, GVL acquire has lock which ensures visibility of ivars
-
# https://github.com/ruby/ruby/blob/ruby_2_2/thread_pthread.c#L204-L211
-
end
-
end
-
-
# @!visibility private
-
# @!macro internal_implementation_note
-
1
class MriObject < AbstractObject
-
1
include MriAttrVolatile
-
-
1
def initialize
-
# nothing to do
-
end
-
end
-
end
-
end
-
1
module Concurrent
-
# noinspection RubyInstanceVariableNamingConvention
-
1
module Synchronization
-
-
# @!visibility private
-
# @!macro internal_implementation_note
-
1
module ConditionSignalling
-
1
protected
-
-
1
def ns_signal
-
@__Condition__.signal
-
self
-
end
-
-
1
def ns_broadcast
-
@__Condition__.broadcast
-
self
-
end
-
end
-
-
-
# @!visibility private
-
# @!macro internal_implementation_note
-
1
class MutexLockableObject < AbstractLockableObject
-
1
include ConditionSignalling
-
-
1
safe_initialization!
-
-
1
def initialize(*defaults)
-
16
super(*defaults)
-
16
@__Lock__ = ::Mutex.new
-
16
@__Condition__ = ::ConditionVariable.new
-
end
-
-
1
protected
-
-
1
def synchronize
-
17
if @__Lock__.owned?
-
5
yield
-
else
-
24
@__Lock__.synchronize { yield }
-
end
-
end
-
-
1
def ns_wait(timeout = nil)
-
@__Condition__.wait @__Lock__, timeout
-
self
-
end
-
end
-
-
# @!visibility private
-
# @!macro internal_implementation_note
-
1
class MonitorLockableObject < AbstractLockableObject
-
1
include ConditionSignalling
-
-
1
safe_initialization!
-
-
1
def initialize(*defaults)
-
super(*defaults)
-
@__Lock__ = ::Monitor.new
-
@__Condition__ = @__Lock__.new_cond
-
end
-
-
1
protected
-
-
1
def synchronize # TODO may be a problem with lock.synchronize { lock.wait }
-
@__Lock__.synchronize { yield }
-
end
-
-
1
def ns_wait(timeout = nil)
-
@__Condition__.wait timeout
-
self
-
end
-
end
-
end
-
end
-
1
module Concurrent
-
1
module Synchronization
-
-
# @!visibility private
-
# @!macro internal_implementation_note
-
ObjectImplementation = case
-
1
when Concurrent.on_cruby?
-
1
MriObject
-
when Concurrent.on_jruby?
-
JRubyObject
-
when Concurrent.on_rbx?
-
RbxObject
-
when Concurrent.on_truffleruby?
-
TruffleRubyObject
-
else
-
warn 'Possibly unsupported Ruby implementation'
-
MriObject
-
end
-
1
private_constant :ObjectImplementation
-
-
# Abstract object providing final, volatile, ans CAS extensions to build other concurrent abstractions.
-
# - final instance variables see {Object.safe_initialization!}
-
# - volatile instance variables see {Object.attr_volatile}
-
# - volatile instance variables see {Object.attr_atomic}
-
1
class Object < ObjectImplementation
-
# TODO make it a module if possible
-
-
# @!method self.attr_volatile(*names)
-
# Creates methods for reading and writing (as `attr_accessor` does) to a instance variable with
-
# volatile (Java) semantic. The instance variable should be accessed only through generated methods.
-
#
-
# @param [::Array<Symbol>] names of the instance variables to be volatile
-
# @return [::Array<Symbol>] names of defined method names
-
-
# Has to be called by children.
-
1
def initialize
-
16
super
-
16
__initialize_atomic_fields__
-
end
-
-
# By calling this method on a class, it and all its children are marked to be constructed safely. Meaning that
-
# all writes (ivar initializations) are made visible to all readers of newly constructed object. It ensures
-
# same behaviour as Java's final fields.
-
# @example
-
# class AClass < Concurrent::Synchronization::Object
-
# safe_initialization!
-
#
-
# def initialize
-
# @AFinalValue = 'value' # published safely, does not have to be synchronized
-
# end
-
# end
-
# @return [true]
-
1
def self.safe_initialization!
-
# define only once, and not again in children
-
23
return if safe_initialization?
-
-
# @!visibility private
-
15
def self.new(*args, &block)
-
17
object = super(*args, &block)
-
ensure
-
17
object.full_memory_barrier if object
-
end
-
-
15
@safe_initialization = true
-
end
-
-
# @return [true, false] if this class is safely initialized.
-
1
def self.safe_initialization?
-
49
@safe_initialization = false unless defined? @safe_initialization
-
49
@safe_initialization || (superclass.respond_to?(:safe_initialization?) && superclass.safe_initialization?)
-
end
-
-
# For testing purposes, quite slow. Injects assert code to new method which will raise if class instance contains
-
# any instance variables with CamelCase names and isn't {.safe_initialization?}.
-
# @raise when offend found
-
# @return [true]
-
1
def self.ensure_safe_initialization_when_final_fields_are_present
-
Object.class_eval do
-
def self.new(*args, &block)
-
object = super(*args, &block)
-
ensure
-
has_final_field = object.instance_variables.any? { |v| v.to_s =~ /^@[A-Z]/ }
-
if has_final_field && !safe_initialization?
-
raise "there was an instance of #{object.class} with final field but not marked with safe_initialization!"
-
end
-
end
-
end
-
true
-
end
-
-
# Creates methods for reading and writing to a instance variable with
-
# volatile (Java) semantic as {.attr_volatile} does.
-
# The instance variable should be accessed oly through generated methods.
-
# This method generates following methods: `value`, `value=(new_value) #=> new_value`,
-
# `swap_value(new_value) #=> old_value`,
-
# `compare_and_set_value(expected, value) #=> true || false`, `update_value(&block)`.
-
# @param [::Array<Symbol>] names of the instance variables to be volatile with CAS.
-
# @return [::Array<Symbol>] names of defined method names.
-
# @!macro attr_atomic
-
# @!method $1
-
# @return [Object] The $1.
-
# @!method $1=(new_$1)
-
# Set the $1.
-
# @return [Object] new_$1.
-
# @!method swap_$1(new_$1)
-
# Set the $1 to new_$1 and return the old $1.
-
# @return [Object] old $1
-
# @!method compare_and_set_$1(expected_$1, new_$1)
-
# Sets the $1 to new_$1 if the current $1 is expected_$1
-
# @return [true, false]
-
# @!method update_$1(&block)
-
# Updates the $1 using the block.
-
# @yield [Object] Calculate a new $1 using given (old) $1
-
# @yieldparam [Object] old $1
-
# @return [Object] new $1
-
1
def self.attr_atomic(*names)
-
6
@__atomic_fields__ ||= []
-
6
@__atomic_fields__ += names
-
6
safe_initialization!
-
6
define_initialize_atomic_fields
-
-
6
names.each do |name|
-
13
ivar = :"@Atomic#{name.to_s.gsub(/(?:^|_)(.)/) { $1.upcase }}"
-
6
class_eval <<-RUBY, __FILE__, __LINE__ + 1
-
def #{name}
-
#{ivar}.get
-
end
-
-
def #{name}=(value)
-
#{ivar}.set value
-
end
-
-
def swap_#{name}(value)
-
#{ivar}.swap value
-
end
-
-
def compare_and_set_#{name}(expected, value)
-
#{ivar}.compare_and_set expected, value
-
end
-
-
def update_#{name}(&block)
-
#{ivar}.update(&block)
-
end
-
RUBY
-
end
-
12
names.flat_map { |n| [n, :"#{n}=", :"swap_#{n}", :"compare_and_set_#{n}", :"update_#{n}"] }
-
end
-
-
# @param [true, false] inherited should inherited volatile with CAS fields be returned?
-
# @return [::Array<Symbol>] Returns defined volatile with CAS fields on this class.
-
1
def self.atomic_attributes(inherited = true)
-
@__atomic_fields__ ||= []
-
((superclass.atomic_attributes if superclass.respond_to?(:atomic_attributes) && inherited) || []) + @__atomic_fields__
-
end
-
-
# @return [true, false] is the attribute with name atomic?
-
1
def self.atomic_attribute?(name)
-
atomic_attributes.include? name
-
end
-
-
1
private
-
-
1
def self.define_initialize_atomic_fields
-
6
assignments = @__atomic_fields__.map do |name|
-
13
"@Atomic#{name.to_s.gsub(/(?:^|_)(.)/) { $1.upcase }} = Concurrent::AtomicReference.new(nil)"
-
end.join("\n")
-
-
6
class_eval <<-RUBY, __FILE__, __LINE__ + 1
-
def __initialize_atomic_fields__
-
super
-
#{assignments}
-
end
-
RUBY
-
end
-
-
1
private_class_method :define_initialize_atomic_fields
-
-
1
def __initialize_atomic_fields__
-
end
-
-
end
-
end
-
end
-
1
module Concurrent
-
1
module Synchronization
-
-
# @!visibility private
-
# @!macro internal_implementation_note
-
1
class RbxLockableObject < AbstractLockableObject
-
1
safe_initialization!
-
-
1
def initialize(*defaults)
-
super(*defaults)
-
@__Waiters__ = []
-
@__owner__ = nil
-
end
-
-
1
protected
-
-
1
def synchronize(&block)
-
if @__owner__ == Thread.current
-
yield
-
else
-
result = nil
-
Rubinius.synchronize(self) do
-
begin
-
@__owner__ = Thread.current
-
result = yield
-
ensure
-
@__owner__ = nil
-
end
-
end
-
result
-
end
-
end
-
-
1
def ns_wait(timeout = nil)
-
wchan = Rubinius::Channel.new
-
-
begin
-
@__Waiters__.push wchan
-
Rubinius.unlock(self)
-
signaled = wchan.receive_timeout timeout
-
ensure
-
Rubinius.lock(self)
-
-
if !signaled && !@__Waiters__.delete(wchan)
-
# we timed out, but got signaled afterwards,
-
# so pass that signal on to the next waiter
-
@__Waiters__.shift << true unless @__Waiters__.empty?
-
end
-
end
-
-
self
-
end
-
-
1
def ns_signal
-
@__Waiters__.shift << true unless @__Waiters__.empty?
-
self
-
end
-
-
1
def ns_broadcast
-
@__Waiters__.shift << true until @__Waiters__.empty?
-
self
-
end
-
end
-
end
-
end
-
1
module Concurrent
-
1
module Synchronization
-
-
# @!visibility private
-
1
module RbxAttrVolatile
-
1
def self.included(base)
-
1
base.extend(ClassMethods)
-
end
-
-
1
module ClassMethods
-
-
1
def attr_volatile(*names)
-
names.each do |name|
-
ivar = :"@volatile_#{name}"
-
class_eval <<-RUBY, __FILE__, __LINE__ + 1
-
def #{name}
-
Rubinius.memory_barrier
-
#{ivar}
-
end
-
-
def #{name}=(value)
-
#{ivar} = value
-
Rubinius.memory_barrier
-
end
-
RUBY
-
end
-
names.map { |n| [n, :"#{n}="] }.flatten
-
end
-
-
end
-
-
1
def full_memory_barrier
-
# Rubinius instance variables are not volatile so we need to insert barrier
-
# TODO (pitr 26-Nov-2015): check comments like ^
-
Rubinius.memory_barrier
-
end
-
end
-
-
# @!visibility private
-
# @!macro internal_implementation_note
-
1
class RbxObject < AbstractObject
-
1
include RbxAttrVolatile
-
-
1
def initialize
-
# nothing to do
-
end
-
end
-
end
-
end
-
1
module Concurrent
-
1
module Synchronization
-
-
# @!visibility private
-
1
module TruffleRubyAttrVolatile
-
1
def self.included(base)
-
1
base.extend(ClassMethods)
-
end
-
-
1
module ClassMethods
-
1
def attr_volatile(*names)
-
names.each do |name|
-
ivar = :"@volatile_#{name}"
-
-
class_eval <<-RUBY, __FILE__, __LINE__ + 1
-
def #{name}
-
full_memory_barrier
-
#{ivar}
-
end
-
-
def #{name}=(value)
-
#{ivar} = value
-
full_memory_barrier
-
end
-
RUBY
-
end
-
-
names.map { |n| [n, :"#{n}="] }.flatten
-
end
-
end
-
-
1
def full_memory_barrier
-
TruffleRuby.full_memory_barrier
-
end
-
end
-
-
# @!visibility private
-
# @!macro internal_implementation_note
-
1
class TruffleRubyObject < AbstractObject
-
1
include TruffleRubyAttrVolatile
-
-
1
def initialize
-
# nothing to do
-
end
-
end
-
end
-
end
-
1
module Concurrent
-
1
module Synchronization
-
-
# Volatile adds the attr_volatile class method when included.
-
#
-
# @example
-
# class Foo
-
# include Concurrent::Synchronization::Volatile
-
#
-
# attr_volatile :bar
-
#
-
# def initialize
-
# self.bar = 1
-
# end
-
# end
-
#
-
# foo = Foo.new
-
# foo.bar
-
# => 1
-
# foo.bar = 2
-
# => 2
-
-
Volatile = case
-
1
when Concurrent.on_cruby?
-
1
MriAttrVolatile
-
when Concurrent.on_jruby?
-
JRubyAttrVolatile
-
when Concurrent.on_rbx?
-
RbxAttrVolatile
-
when Concurrent.on_truffleruby?
-
TruffleRubyAttrVolatile
-
else
-
MriAttrVolatile
-
end
-
end
-
end
-
1
require 'delegate'
-
1
require 'monitor'
-
-
1
module Concurrent
-
1
unless defined?(SynchronizedDelegator)
-
-
# This class provides a trivial way to synchronize all calls to a given object
-
# by wrapping it with a `Delegator` that performs `Monitor#enter/exit` calls
-
# around the delegated `#send`. Example:
-
#
-
# array = [] # not thread-safe on many impls
-
# array = SynchronizedDelegator.new([]) # thread-safe
-
#
-
# A simple `Monitor` provides a very coarse-grained way to synchronize a given
-
# object, in that it will cause synchronization for methods that have no need
-
# for it, but this is a trivial way to get thread-safety where none may exist
-
# currently on some implementations.
-
#
-
# This class is currently being considered for inclusion into stdlib, via
-
# https://bugs.ruby-lang.org/issues/8556
-
#
-
# @!visibility private
-
1
class SynchronizedDelegator < SimpleDelegator
-
1
def setup
-
@old_abort = Thread.abort_on_exception
-
Thread.abort_on_exception = true
-
end
-
-
1
def teardown
-
Thread.abort_on_exception = @old_abort
-
end
-
-
1
def initialize(obj)
-
__setobj__(obj)
-
@monitor = Monitor.new
-
end
-
-
1
def method_missing(method, *args, &block)
-
monitor = @monitor
-
begin
-
monitor.enter
-
super
-
ensure
-
monitor.exit
-
end
-
end
-
-
end
-
end
-
end
-
1
module Concurrent
-
-
# @!visibility private
-
1
module ThreadSafe
-
-
# @!visibility private
-
1
module Util
-
-
# TODO (pitr-ch 15-Oct-2016): migrate to Utility::NativeInteger
-
1
FIXNUM_BIT_SIZE = (0.size * 8) - 2
-
1
MAX_INT = (2 ** FIXNUM_BIT_SIZE) - 1
-
# TODO (pitr-ch 15-Oct-2016): migrate to Utility::ProcessorCounter
-
1
CPU_COUNT = 16 # is there a way to determine this?
-
end
-
end
-
end
-
1
require 'concurrent/collection/copy_on_notify_observer_set'
-
1
require 'concurrent/concern/dereferenceable'
-
1
require 'concurrent/concern/observable'
-
1
require 'concurrent/atomic/atomic_boolean'
-
1
require 'concurrent/executor/executor_service'
-
1
require 'concurrent/executor/ruby_executor_service'
-
1
require 'concurrent/executor/safe_task_executor'
-
1
require 'concurrent/scheduled_task'
-
-
1
module Concurrent
-
-
# A very common concurrency pattern is to run a thread that performs a task at
-
# regular intervals. The thread that performs the task sleeps for the given
-
# interval then wakes up and performs the task. Lather, rinse, repeat... This
-
# pattern causes two problems. First, it is difficult to test the business
-
# logic of the task because the task itself is tightly coupled with the
-
# concurrency logic. Second, an exception raised while performing the task can
-
# cause the entire thread to abend. In a long-running application where the
-
# task thread is intended to run for days/weeks/years a crashed task thread
-
# can pose a significant problem. `TimerTask` alleviates both problems.
-
#
-
# When a `TimerTask` is launched it starts a thread for monitoring the
-
# execution interval. The `TimerTask` thread does not perform the task,
-
# however. Instead, the TimerTask launches the task on a separate thread.
-
# Should the task experience an unrecoverable crash only the task thread will
-
# crash. This makes the `TimerTask` very fault tolerant. Additionally, the
-
# `TimerTask` thread can respond to the success or failure of the task,
-
# performing logging or ancillary operations. `TimerTask` can also be
-
# configured with a timeout value allowing it to kill a task that runs too
-
# long.
-
#
-
# One other advantage of `TimerTask` is that it forces the business logic to
-
# be completely decoupled from the concurrency logic. The business logic can
-
# be tested separately then passed to the `TimerTask` for scheduling and
-
# running.
-
#
-
# In some cases it may be necessary for a `TimerTask` to affect its own
-
# execution cycle. To facilitate this, a reference to the TimerTask instance
-
# is passed as an argument to the provided block every time the task is
-
# executed.
-
#
-
# The `TimerTask` class includes the `Dereferenceable` mixin module so the
-
# result of the last execution is always available via the `#value` method.
-
# Dereferencing options can be passed to the `TimerTask` during construction or
-
# at any later time using the `#set_deref_options` method.
-
#
-
# `TimerTask` supports notification through the Ruby standard library
-
# {http://ruby-doc.org/stdlib-2.0/libdoc/observer/rdoc/Observable.html
-
# Observable} module. On execution the `TimerTask` will notify the observers
-
# with three arguments: time of execution, the result of the block (or nil on
-
# failure), and any raised exceptions (or nil on success). If the timeout
-
# interval is exceeded the observer will receive a `Concurrent::TimeoutError`
-
# object as the third argument.
-
#
-
# @!macro copy_options
-
#
-
# @example Basic usage
-
# task = Concurrent::TimerTask.new{ puts 'Boom!' }
-
# task.execute
-
#
-
# task.execution_interval #=> 60 (default)
-
# task.timeout_interval #=> 30 (default)
-
#
-
# # wait 60 seconds...
-
# #=> 'Boom!'
-
#
-
# task.shutdown #=> true
-
#
-
# @example Configuring `:execution_interval` and `:timeout_interval`
-
# task = Concurrent::TimerTask.new(execution_interval: 5, timeout_interval: 5) do
-
# puts 'Boom!'
-
# end
-
#
-
# task.execution_interval #=> 5
-
# task.timeout_interval #=> 5
-
#
-
# @example Immediate execution with `:run_now`
-
# task = Concurrent::TimerTask.new(run_now: true){ puts 'Boom!' }
-
# task.execute
-
#
-
# #=> 'Boom!'
-
#
-
# @example Last `#value` and `Dereferenceable` mixin
-
# task = Concurrent::TimerTask.new(
-
# dup_on_deref: true,
-
# execution_interval: 5
-
# ){ Time.now }
-
#
-
# task.execute
-
# Time.now #=> 2013-11-07 18:06:50 -0500
-
# sleep(10)
-
# task.value #=> 2013-11-07 18:06:55 -0500
-
#
-
# @example Controlling execution from within the block
-
# timer_task = Concurrent::TimerTask.new(execution_interval: 1) do |task|
-
# task.execution_interval.times{ print 'Boom! ' }
-
# print "\n"
-
# task.execution_interval += 1
-
# if task.execution_interval > 5
-
# puts 'Stopping...'
-
# task.shutdown
-
# end
-
# end
-
#
-
# timer_task.execute # blocking call - this task will stop itself
-
# #=> Boom!
-
# #=> Boom! Boom!
-
# #=> Boom! Boom! Boom!
-
# #=> Boom! Boom! Boom! Boom!
-
# #=> Boom! Boom! Boom! Boom! Boom!
-
# #=> Stopping...
-
#
-
# @example Observation
-
# class TaskObserver
-
# def update(time, result, ex)
-
# if result
-
# print "(#{time}) Execution successfully returned #{result}\n"
-
# elsif ex.is_a?(Concurrent::TimeoutError)
-
# print "(#{time}) Execution timed out\n"
-
# else
-
# print "(#{time}) Execution failed with error #{ex}\n"
-
# end
-
# end
-
# end
-
#
-
# task = Concurrent::TimerTask.new(execution_interval: 1, timeout_interval: 1){ 42 }
-
# task.add_observer(TaskObserver.new)
-
# task.execute
-
# sleep 4
-
#
-
# #=> (2013-10-13 19:08:58 -0400) Execution successfully returned 42
-
# #=> (2013-10-13 19:08:59 -0400) Execution successfully returned 42
-
# #=> (2013-10-13 19:09:00 -0400) Execution successfully returned 42
-
# task.shutdown
-
#
-
# task = Concurrent::TimerTask.new(execution_interval: 1, timeout_interval: 1){ sleep }
-
# task.add_observer(TaskObserver.new)
-
# task.execute
-
#
-
# #=> (2013-10-13 19:07:25 -0400) Execution timed out
-
# #=> (2013-10-13 19:07:27 -0400) Execution timed out
-
# #=> (2013-10-13 19:07:29 -0400) Execution timed out
-
# task.shutdown
-
#
-
# task = Concurrent::TimerTask.new(execution_interval: 1){ raise StandardError }
-
# task.add_observer(TaskObserver.new)
-
# task.execute
-
#
-
# #=> (2013-10-13 19:09:37 -0400) Execution failed with error StandardError
-
# #=> (2013-10-13 19:09:38 -0400) Execution failed with error StandardError
-
# #=> (2013-10-13 19:09:39 -0400) Execution failed with error StandardError
-
# task.shutdown
-
#
-
# @see http://ruby-doc.org/stdlib-2.0/libdoc/observer/rdoc/Observable.html
-
# @see http://docs.oracle.com/javase/7/docs/api/java/util/TimerTask.html
-
1
class TimerTask < RubyExecutorService
-
1
include Concern::Dereferenceable
-
1
include Concern::Observable
-
-
# Default `:execution_interval` in seconds.
-
1
EXECUTION_INTERVAL = 60
-
-
# Default `:timeout_interval` in seconds.
-
1
TIMEOUT_INTERVAL = 30
-
-
# Create a new TimerTask with the given task and configuration.
-
#
-
# @!macro timer_task_initialize
-
# @param [Hash] opts the options defining task execution.
-
# @option opts [Integer] :execution_interval number of seconds between
-
# task executions (default: EXECUTION_INTERVAL)
-
# @option opts [Integer] :timeout_interval number of seconds a task can
-
# run before it is considered to have failed (default: TIMEOUT_INTERVAL)
-
# @option opts [Boolean] :run_now Whether to run the task immediately
-
# upon instantiation or to wait until the first # execution_interval
-
# has passed (default: false)
-
#
-
# @!macro deref_options
-
#
-
# @raise ArgumentError when no block is given.
-
#
-
# @yield to the block after :execution_interval seconds have passed since
-
# the last yield
-
# @yieldparam task a reference to the `TimerTask` instance so that the
-
# block can control its own lifecycle. Necessary since `self` will
-
# refer to the execution context of the block rather than the running
-
# `TimerTask`.
-
#
-
# @return [TimerTask] the new `TimerTask`
-
1
def initialize(opts = {}, &task)
-
raise ArgumentError.new('no block given') unless block_given?
-
super
-
set_deref_options opts
-
end
-
-
# Is the executor running?
-
#
-
# @return [Boolean] `true` when running, `false` when shutting down or shutdown
-
1
def running?
-
@running.true?
-
end
-
-
# Execute a previously created `TimerTask`.
-
#
-
# @return [TimerTask] a reference to `self`
-
#
-
# @example Instance and execute in separate steps
-
# task = Concurrent::TimerTask.new(execution_interval: 10){ print "Hello World\n" }
-
# task.running? #=> false
-
# task.execute
-
# task.running? #=> true
-
#
-
# @example Instance and execute in one line
-
# task = Concurrent::TimerTask.new(execution_interval: 10){ print "Hello World\n" }.execute
-
# task.running? #=> true
-
1
def execute
-
synchronize do
-
if @running.false?
-
@running.make_true
-
schedule_next_task(@run_now ? 0 : @execution_interval)
-
end
-
end
-
self
-
end
-
-
# Create and execute a new `TimerTask`.
-
#
-
# @!macro timer_task_initialize
-
#
-
# @example
-
# task = Concurrent::TimerTask.execute(execution_interval: 10){ print "Hello World\n" }
-
# task.running? #=> true
-
1
def self.execute(opts = {}, &task)
-
TimerTask.new(opts, &task).execute
-
end
-
-
# @!attribute [rw] execution_interval
-
# @return [Fixnum] Number of seconds after the task completes before the
-
# task is performed again.
-
1
def execution_interval
-
synchronize { @execution_interval }
-
end
-
-
# @!attribute [rw] execution_interval
-
# @return [Fixnum] Number of seconds after the task completes before the
-
# task is performed again.
-
1
def execution_interval=(value)
-
if (value = value.to_f) <= 0.0
-
raise ArgumentError.new('must be greater than zero')
-
else
-
synchronize { @execution_interval = value }
-
end
-
end
-
-
# @!attribute [rw] timeout_interval
-
# @return [Fixnum] Number of seconds the task can run before it is
-
# considered to have failed.
-
1
def timeout_interval
-
synchronize { @timeout_interval }
-
end
-
-
# @!attribute [rw] timeout_interval
-
# @return [Fixnum] Number of seconds the task can run before it is
-
# considered to have failed.
-
1
def timeout_interval=(value)
-
if (value = value.to_f) <= 0.0
-
raise ArgumentError.new('must be greater than zero')
-
else
-
synchronize { @timeout_interval = value }
-
end
-
end
-
-
1
private :post, :<<
-
-
1
private
-
-
1
def ns_initialize(opts, &task)
-
set_deref_options(opts)
-
-
self.execution_interval = opts[:execution] || opts[:execution_interval] || EXECUTION_INTERVAL
-
self.timeout_interval = opts[:timeout] || opts[:timeout_interval] || TIMEOUT_INTERVAL
-
@run_now = opts[:now] || opts[:run_now]
-
@executor = Concurrent::SafeTaskExecutor.new(task)
-
@running = Concurrent::AtomicBoolean.new(false)
-
@value = nil
-
-
self.observers = Collection::CopyOnNotifyObserverSet.new
-
end
-
-
# @!visibility private
-
1
def ns_shutdown_execution
-
@running.make_false
-
super
-
end
-
-
# @!visibility private
-
1
def ns_kill_execution
-
@running.make_false
-
super
-
end
-
-
# @!visibility private
-
1
def schedule_next_task(interval = execution_interval)
-
ScheduledTask.execute(interval, args: [Concurrent::Event.new], &method(:execute_task))
-
nil
-
end
-
-
# @!visibility private
-
1
def execute_task(completion)
-
return nil unless @running.true?
-
ScheduledTask.execute(timeout_interval, args: [completion], &method(:timeout_task))
-
_success, value, reason = @executor.execute(self)
-
if completion.try?
-
self.value = value
-
schedule_next_task
-
time = Time.now
-
observers.notify_observers do
-
[time, self.value, reason]
-
end
-
end
-
nil
-
end
-
-
# @!visibility private
-
1
def timeout_task(completion)
-
return unless @running.true?
-
if completion.try?
-
self.value = value
-
schedule_next_task
-
observers.notify_observers(Time.now, nil, Concurrent::TimeoutError.new)
-
end
-
end
-
end
-
end
-
1
require 'concurrent/atomic/atomic_reference'
-
-
1
module Concurrent
-
-
# A fixed size array with volatile (synchronized, thread safe) getters/setters.
-
# Mixes in Ruby's `Enumerable` module for enhanced search, sort, and traversal.
-
#
-
# @example
-
# tuple = Concurrent::Tuple.new(16)
-
#
-
# tuple.set(0, :foo) #=> :foo | volatile write
-
# tuple.get(0) #=> :foo | volatile read
-
# tuple.compare_and_set(0, :foo, :bar) #=> true | strong CAS
-
# tuple.cas(0, :foo, :baz) #=> false | strong CAS
-
# tuple.get(0) #=> :bar | volatile read
-
#
-
# @see https://en.wikipedia.org/wiki/Tuple Tuple entry at Wikipedia
-
# @see http://www.erlang.org/doc/reference_manual/data_types.html#id70396 Erlang Tuple
-
# @see http://ruby-doc.org/core-2.2.2/Enumerable.html Enumerable
-
1
class Tuple
-
1
include Enumerable
-
-
# The (fixed) size of the tuple.
-
1
attr_reader :size
-
-
# @!visibility private
-
1
Tuple = defined?(Rubinius::Tuple) ? Rubinius::Tuple : ::Array
-
1
private_constant :Tuple
-
-
# Create a new tuple of the given size.
-
#
-
# @param [Integer] size the number of elements in the tuple
-
1
def initialize(size)
-
@size = size
-
@tuple = tuple = Tuple.new(size)
-
i = 0
-
while i < size
-
tuple[i] = Concurrent::AtomicReference.new
-
i += 1
-
end
-
end
-
-
# Get the value of the element at the given index.
-
#
-
# @param [Integer] i the index from which to retrieve the value
-
# @return [Object] the value at the given index or nil if the index is out of bounds
-
1
def get(i)
-
return nil if i >= @size || i < 0
-
@tuple[i].get
-
end
-
1
alias_method :volatile_get, :get
-
-
# Set the element at the given index to the given value
-
#
-
# @param [Integer] i the index for the element to set
-
# @param [Object] value the value to set at the given index
-
#
-
# @return [Object] the new value of the element at the given index or nil if the index is out of bounds
-
1
def set(i, value)
-
return nil if i >= @size || i < 0
-
@tuple[i].set(value)
-
end
-
1
alias_method :volatile_set, :set
-
-
# Set the value at the given index to the new value if and only if the current
-
# value matches the given old value.
-
#
-
# @param [Integer] i the index for the element to set
-
# @param [Object] old_value the value to compare against the current value
-
# @param [Object] new_value the value to set at the given index
-
#
-
# @return [Boolean] true if the value at the given element was set else false
-
1
def compare_and_set(i, old_value, new_value)
-
return false if i >= @size || i < 0
-
@tuple[i].compare_and_set(old_value, new_value)
-
end
-
1
alias_method :cas, :compare_and_set
-
-
# Calls the given block once for each element in self, passing that element as a parameter.
-
#
-
# @yieldparam [Object] ref the `Concurrent::AtomicReference` object at the current index
-
1
def each
-
@tuple.each {|ref| yield ref.get}
-
end
-
end
-
end
-
1
require 'set'
-
1
require 'concurrent/synchronization'
-
-
1
module Concurrent
-
-
# A `TVar` is a transactional variable - a single-element container that
-
# is used as part of a transaction - see `Concurrent::atomically`.
-
#
-
# @!macro thread_safe_variable_comparison
-
#
-
# {include:file:docs-source/tvar.md}
-
1
class TVar < Synchronization::Object
-
1
safe_initialization!
-
-
# Create a new `TVar` with an initial value.
-
1
def initialize(value)
-
@value = value
-
@version = 0
-
@lock = Mutex.new
-
end
-
-
# Get the value of a `TVar`.
-
1
def value
-
Concurrent::atomically do
-
Transaction::current.read(self)
-
end
-
end
-
-
# Set the value of a `TVar`.
-
1
def value=(value)
-
Concurrent::atomically do
-
Transaction::current.write(self, value)
-
end
-
end
-
-
# @!visibility private
-
1
def unsafe_value # :nodoc:
-
@value
-
end
-
-
# @!visibility private
-
1
def unsafe_value=(value) # :nodoc:
-
@value = value
-
end
-
-
# @!visibility private
-
1
def unsafe_version # :nodoc:
-
@version
-
end
-
-
# @!visibility private
-
1
def unsafe_increment_version # :nodoc:
-
@version += 1
-
end
-
-
# @!visibility private
-
1
def unsafe_lock # :nodoc:
-
@lock
-
end
-
-
end
-
-
# Run a block that reads and writes `TVar`s as a single atomic transaction.
-
# With respect to the value of `TVar` objects, the transaction is atomic, in
-
# that it either happens or it does not, consistent, in that the `TVar`
-
# objects involved will never enter an illegal state, and isolated, in that
-
# transactions never interfere with each other. You may recognise these
-
# properties from database transactions.
-
#
-
# There are some very important and unusual semantics that you must be aware of:
-
#
-
# * Most importantly, the block that you pass to atomically may be executed
-
# more than once. In most cases your code should be free of
-
# side-effects, except for via TVar.
-
#
-
# * If an exception escapes an atomically block it will abort the transaction.
-
#
-
# * It is undefined behaviour to use callcc or Fiber with atomically.
-
#
-
# * If you create a new thread within an atomically, it will not be part of
-
# the transaction. Creating a thread counts as a side-effect.
-
#
-
# Transactions within transactions are flattened to a single transaction.
-
#
-
# @example
-
# a = new TVar(100_000)
-
# b = new TVar(100)
-
#
-
# Concurrent::atomically do
-
# a.value -= 10
-
# b.value += 10
-
# end
-
1
def atomically
-
raise ArgumentError.new('no block given') unless block_given?
-
-
# Get the current transaction
-
-
transaction = Transaction::current
-
-
# Are we not already in a transaction (not nested)?
-
-
if transaction.nil?
-
# New transaction
-
-
begin
-
# Retry loop
-
-
loop do
-
-
# Create a new transaction
-
-
transaction = Transaction.new
-
Transaction::current = transaction
-
-
# Run the block, aborting on exceptions
-
-
begin
-
result = yield
-
rescue Transaction::AbortError => e
-
transaction.abort
-
result = Transaction::ABORTED
-
rescue Transaction::LeaveError => e
-
transaction.abort
-
break result
-
rescue => e
-
transaction.abort
-
raise e
-
end
-
# If we can commit, break out of the loop
-
-
if result != Transaction::ABORTED
-
if transaction.commit
-
break result
-
end
-
end
-
end
-
ensure
-
# Clear the current transaction
-
-
Transaction::current = nil
-
end
-
else
-
# Nested transaction - flatten it and just run the block
-
-
yield
-
end
-
end
-
-
# Abort a currently running transaction - see `Concurrent::atomically`.
-
1
def abort_transaction
-
raise Transaction::AbortError.new
-
end
-
-
# Leave a transaction without committing or aborting - see `Concurrent::atomically`.
-
1
def leave_transaction
-
raise Transaction::LeaveError.new
-
end
-
-
1
module_function :atomically, :abort_transaction, :leave_transaction
-
-
1
private
-
-
1
class Transaction
-
-
1
ABORTED = ::Object.new
-
-
1
ReadLogEntry = Struct.new(:tvar, :version)
-
-
1
AbortError = Class.new(StandardError)
-
1
LeaveError = Class.new(StandardError)
-
-
1
def initialize
-
@read_log = []
-
@write_log = {}
-
end
-
-
1
def read(tvar)
-
Concurrent::abort_transaction unless valid?
-
-
if @write_log.has_key? tvar
-
@write_log[tvar]
-
else
-
@read_log.push(ReadLogEntry.new(tvar, tvar.unsafe_version))
-
tvar.unsafe_value
-
end
-
end
-
-
1
def write(tvar, value)
-
# Have we already written to this TVar?
-
-
unless @write_log.has_key? tvar
-
# Try to lock the TVar
-
-
unless tvar.unsafe_lock.try_lock
-
# Someone else is writing to this TVar - abort
-
Concurrent::abort_transaction
-
end
-
-
# If we previously wrote to it, check the version hasn't changed
-
-
@read_log.each do |log_entry|
-
if log_entry.tvar == tvar and tvar.unsafe_version > log_entry.version
-
Concurrent::abort_transaction
-
end
-
end
-
end
-
-
# Record the value written
-
-
@write_log[tvar] = value
-
end
-
-
1
def abort
-
unlock
-
end
-
-
1
def commit
-
return false unless valid?
-
-
@write_log.each_pair do |tvar, value|
-
tvar.unsafe_value = value
-
tvar.unsafe_increment_version
-
end
-
-
unlock
-
-
true
-
end
-
-
1
def valid?
-
@read_log.each do |log_entry|
-
unless @write_log.has_key? log_entry.tvar
-
if log_entry.tvar.unsafe_version > log_entry.version
-
return false
-
end
-
end
-
end
-
-
true
-
end
-
-
1
def unlock
-
@write_log.each_key do |tvar|
-
tvar.unsafe_lock.unlock
-
end
-
end
-
-
1
def self.current
-
Thread.current[:current_tvar_transaction]
-
end
-
-
1
def self.current=(transaction)
-
Thread.current[:current_tvar_transaction] = transaction
-
end
-
-
end
-
-
end
-
1
module Concurrent
-
1
module Utility
-
-
# @!visibility private
-
1
module EngineDetector
-
1
def on_jruby?
-
20
ruby_engine == 'jruby'
-
end
-
-
1
def on_jruby_9000?
-
on_jruby? && ruby_version(JRUBY_VERSION, :>=, 9, 0, 0)
-
end
-
-
1
def on_cruby?
-
10
ruby_engine == 'ruby'
-
end
-
-
1
def on_rbx?
-
1
ruby_engine == 'rbx'
-
end
-
-
1
def on_truffleruby?
-
2
ruby_engine == 'truffleruby'
-
end
-
-
1
def on_windows?
-
!(RbConfig::CONFIG['host_os'] =~ /mswin|mingw|cygwin/).nil?
-
end
-
-
1
def on_osx?
-
!(RbConfig::CONFIG['host_os'] =~ /darwin|mac os/).nil?
-
end
-
-
1
def on_linux?
-
!(RbConfig::CONFIG['host_os'] =~ /linux/).nil?
-
end
-
-
1
def ruby_engine
-
33
defined?(RUBY_ENGINE) ? RUBY_ENGINE : 'ruby'
-
end
-
-
1
def ruby_version(version = RUBY_VERSION, comparison, major, minor, patch)
-
2
result = (version.split('.').map(&:to_i) <=> [major, minor, patch])
-
2
comparisons = { :== => [0],
-
:>= => [1, 0],
-
:<= => [-1, 0],
-
:> => [1],
-
:< => [-1] }
-
2
comparisons.fetch(comparison).include? result
-
end
-
end
-
end
-
-
# @!visibility private
-
1
extend Utility::EngineDetector
-
end
-
1
require 'concurrent/synchronization'
-
-
1
module Concurrent
-
-
1
class_definition = Class.new(Synchronization::LockableObject) do
-
1
def initialize
-
1
@last_time = Time.now.to_f
-
1
super()
-
end
-
-
1
if defined?(Process::CLOCK_MONOTONIC)
-
# @!visibility private
-
1
def get_time
-
Process.clock_gettime(Process::CLOCK_MONOTONIC)
-
end
-
elsif Concurrent.on_jruby?
-
# @!visibility private
-
def get_time
-
java.lang.System.nanoTime() / 1_000_000_000.0
-
end
-
else
-
-
# @!visibility private
-
def get_time
-
synchronize do
-
now = Time.now.to_f
-
if @last_time < now
-
@last_time = now
-
else # clock has moved back in time
-
@last_time += 0.000_001
-
end
-
end
-
end
-
-
end
-
end
-
-
# Clock that cannot be set and represents monotonic time since
-
# some unspecified starting point.
-
#
-
# @!visibility private
-
1
GLOBAL_MONOTONIC_CLOCK = class_definition.new
-
1
private_constant :GLOBAL_MONOTONIC_CLOCK
-
-
# @!macro monotonic_get_time
-
#
-
# Returns the current time a tracked by the application monotonic clock.
-
#
-
# @return [Float] The current monotonic time since some unspecified
-
# starting point
-
#
-
# @!macro monotonic_clock_warning
-
1
def monotonic_time
-
GLOBAL_MONOTONIC_CLOCK.get_time
-
end
-
-
1
module_function :monotonic_time
-
end
-
1
require 'concurrent/utility/engine'
-
-
1
module Concurrent
-
-
1
module Utility
-
-
# @!visibility private
-
1
module NativeExtensionLoader
-
-
1
def allow_c_extensions?
-
Concurrent.on_cruby?
-
end
-
-
1
def c_extensions_loaded?
-
2
defined?(@c_extensions_loaded) && @c_extensions_loaded
-
end
-
-
1
def java_extensions_loaded?
-
defined?(@java_extensions_loaded) && @java_extensions_loaded
-
end
-
-
1
def load_native_extensions
-
1
unless defined? Synchronization::AbstractObject
-
raise 'native_extension_loader loaded before Synchronization::AbstractObject'
-
end
-
-
1
if Concurrent.on_cruby? && !c_extensions_loaded?
-
1
['concurrent/concurrent_ruby_ext',
-
"concurrent/#{RUBY_VERSION[0..2]}/concurrent_ruby_ext"
-
2
].each { |p| try_load_c_extension p }
-
end
-
-
1
if Concurrent.on_jruby? && !java_extensions_loaded?
-
begin
-
require 'concurrent/concurrent_ruby.jar'
-
set_java_extensions_loaded
-
rescue LoadError => e
-
raise e, "Java extensions are required for JRuby.\n" + e.message, e.backtrace
-
end
-
end
-
end
-
-
1
private
-
-
1
def load_error_path(error)
-
2
if error.respond_to? :path
-
2
error.path
-
else
-
error.message.split(' -- ').last
-
end
-
end
-
-
1
def set_c_extensions_loaded
-
@c_extensions_loaded = true
-
end
-
-
1
def set_java_extensions_loaded
-
@java_extensions_loaded = true
-
end
-
-
1
def try_load_c_extension(path)
-
2
require path
-
set_c_extensions_loaded
-
rescue LoadError => e
-
2
if load_error_path(e) == path
-
# move on with pure-Ruby implementations
-
# TODO (pitr-ch 12-Jul-2018): warning on verbose?
-
else
-
raise e
-
end
-
end
-
-
end
-
end
-
-
# @!visibility private
-
1
extend Utility::NativeExtensionLoader
-
end
-
-
1
module Concurrent
-
1
module Utility
-
# @private
-
1
module NativeInteger
-
# http://stackoverflow.com/questions/535721/ruby-max-integer
-
1
MIN_VALUE = -(2**(0.size * 8 - 2))
-
1
MAX_VALUE = (2**(0.size * 8 - 2) - 1)
-
-
1
def ensure_upper_bound(value)
-
if value > MAX_VALUE
-
raise RangeError.new("#{value} is greater than the maximum value of #{MAX_VALUE}")
-
end
-
value
-
end
-
-
1
def ensure_lower_bound(value)
-
if value < MIN_VALUE
-
raise RangeError.new("#{value} is less than the maximum value of #{MIN_VALUE}")
-
end
-
value
-
end
-
-
1
def ensure_integer(value)
-
unless value.is_a?(Integer)
-
raise ArgumentError.new("#{value} is not an Integer")
-
end
-
value
-
end
-
-
1
def ensure_integer_and_bounds(value)
-
ensure_integer value
-
ensure_upper_bound value
-
ensure_lower_bound value
-
end
-
-
1
def ensure_positive(value)
-
if value < 0
-
raise ArgumentError.new("#{value} cannot be negative")
-
end
-
value
-
end
-
-
1
def ensure_positive_and_no_zero(value)
-
if value < 1
-
raise ArgumentError.new("#{value} cannot be negative or zero")
-
end
-
value
-
end
-
-
1
extend self
-
end
-
end
-
end
-
1
require 'etc'
-
1
require 'rbconfig'
-
1
require 'concurrent/delay'
-
-
1
module Concurrent
-
1
module Utility
-
-
# @!visibility private
-
1
class ProcessorCounter
-
1
def initialize
-
1
@processor_count = Delay.new { compute_processor_count }
-
1
@physical_processor_count = Delay.new { compute_physical_processor_count }
-
end
-
-
# Number of processors seen by the OS and used for process scheduling. For
-
# performance reasons the calculated value will be memoized on the first
-
# call.
-
#
-
# When running under JRuby the Java runtime call
-
# `java.lang.Runtime.getRuntime.availableProcessors` will be used. According
-
# to the Java documentation this "value may change during a particular
-
# invocation of the virtual machine... [applications] should therefore
-
# occasionally poll this property." Subsequently the result will NOT be
-
# memoized under JRuby.
-
#
-
# Ruby's Etc.nprocessors will be used if available (MRI 2.2+).
-
#
-
# On Windows the Win32 API will be queried for the
-
# `NumberOfLogicalProcessors from Win32_Processor`. This will return the
-
# total number "logical processors for the current instance of the
-
# processor", which taked into account hyperthreading.
-
#
-
# * AIX: /usr/sbin/pmcycles (AIX 5+), /usr/sbin/lsdev
-
# * Alpha: /usr/bin/nproc (/proc/cpuinfo exists but cannot be used)
-
# * BSD: /sbin/sysctl
-
# * Cygwin: /proc/cpuinfo
-
# * Darwin: /usr/bin/hwprefs, /usr/sbin/sysctl
-
# * HP-UX: /usr/sbin/ioscan
-
# * IRIX: /usr/sbin/sysconf
-
# * Linux: /proc/cpuinfo
-
# * Minix 3+: /proc/cpuinfo
-
# * Solaris: /usr/sbin/psrinfo
-
# * Tru64 UNIX: /usr/sbin/psrinfo
-
# * UnixWare: /usr/sbin/psrinfo
-
#
-
# @return [Integer] number of processors seen by the OS or Java runtime
-
#
-
# @see https://github.com/grosser/parallel/blob/4fc8b89d08c7091fe0419ca8fba1ec3ce5a8d185/lib/parallel.rb
-
#
-
# @see http://docs.oracle.com/javase/6/docs/api/java/lang/Runtime.html#availableProcessors()
-
# @see http://msdn.microsoft.com/en-us/library/aa394373(v=vs.85).aspx
-
1
def processor_count
-
@processor_count.value
-
end
-
-
# Number of physical processor cores on the current system. For performance
-
# reasons the calculated value will be memoized on the first call.
-
#
-
# On Windows the Win32 API will be queried for the `NumberOfCores from
-
# Win32_Processor`. This will return the total number "of cores for the
-
# current instance of the processor." On Unix-like operating systems either
-
# the `hwprefs` or `sysctl` utility will be called in a subshell and the
-
# returned value will be used. In the rare case where none of these methods
-
# work or an exception is raised the function will simply return 1.
-
#
-
# @return [Integer] number physical processor cores on the current system
-
#
-
# @see https://github.com/grosser/parallel/blob/4fc8b89d08c7091fe0419ca8fba1ec3ce5a8d185/lib/parallel.rb
-
#
-
# @see http://msdn.microsoft.com/en-us/library/aa394373(v=vs.85).aspx
-
# @see http://www.unix.com/man-page/osx/1/HWPREFS/
-
# @see http://linux.die.net/man/8/sysctl
-
1
def physical_processor_count
-
@physical_processor_count.value
-
end
-
-
1
private
-
-
1
def compute_processor_count
-
if Concurrent.on_jruby?
-
java.lang.Runtime.getRuntime.availableProcessors
-
elsif Etc.respond_to?(:nprocessors) && (nprocessor = Etc.nprocessors rescue nil)
-
nprocessor
-
else
-
os_name = RbConfig::CONFIG["target_os"]
-
if os_name =~ /mingw|mswin/
-
require 'win32ole'
-
result = WIN32OLE.connect("winmgmts://").ExecQuery(
-
"select NumberOfLogicalProcessors from Win32_Processor")
-
result.to_enum.collect(&:NumberOfLogicalProcessors).reduce(:+)
-
elsif File.readable?("/proc/cpuinfo") && (cpuinfo_count = IO.read("/proc/cpuinfo").scan(/^processor/).size) > 0
-
cpuinfo_count
-
elsif File.executable?("/usr/bin/nproc")
-
IO.popen("/usr/bin/nproc --all", &:read).to_i
-
elsif File.executable?("/usr/bin/hwprefs")
-
IO.popen("/usr/bin/hwprefs thread_count", &:read).to_i
-
elsif File.executable?("/usr/sbin/psrinfo")
-
IO.popen("/usr/sbin/psrinfo", &:read).scan(/^.*on-*line/).size
-
elsif File.executable?("/usr/sbin/ioscan")
-
IO.popen("/usr/sbin/ioscan -kC processor", &:read).scan(/^.*processor/).size
-
elsif File.executable?("/usr/sbin/pmcycles")
-
IO.popen("/usr/sbin/pmcycles -m", &:read).count("\n")
-
elsif File.executable?("/usr/sbin/lsdev")
-
IO.popen("/usr/sbin/lsdev -Cc processor -S 1", &:read).count("\n")
-
elsif File.executable?("/usr/sbin/sysconf") and os_name =~ /irix/i
-
IO.popen("/usr/sbin/sysconf NPROC_ONLN", &:read).to_i
-
elsif File.executable?("/usr/sbin/sysctl")
-
IO.popen("/usr/sbin/sysctl -n hw.ncpu", &:read).to_i
-
elsif File.executable?("/sbin/sysctl")
-
IO.popen("/sbin/sysctl -n hw.ncpu", &:read).to_i
-
else
-
# TODO (pitr-ch 05-Nov-2016): warn about failures
-
1
-
end
-
end
-
rescue
-
return 1
-
end
-
-
1
def compute_physical_processor_count
-
ppc = case RbConfig::CONFIG["target_os"]
-
when /darwin1/
-
IO.popen("/usr/sbin/sysctl -n hw.physicalcpu", &:read).to_i
-
when /linux/
-
cores = {} # unique physical ID / core ID combinations
-
phy = 0
-
IO.read("/proc/cpuinfo").scan(/^physical id.*|^core id.*/) do |ln|
-
if ln.start_with?("physical")
-
phy = ln[/\d+/]
-
elsif ln.start_with?("core")
-
cid = phy + ":" + ln[/\d+/]
-
cores[cid] = true if not cores[cid]
-
end
-
end
-
cores.count
-
when /mswin|mingw/
-
require 'win32ole'
-
result_set = WIN32OLE.connect("winmgmts://").ExecQuery(
-
"select NumberOfCores from Win32_Processor")
-
result_set.to_enum.collect(&:NumberOfCores).reduce(:+)
-
else
-
processor_count
-
end
-
# fall back to logical count if physical info is invalid
-
ppc > 0 ? ppc : processor_count
-
rescue
-
return 1
-
end
-
end
-
end
-
-
# create the default ProcessorCounter on load
-
1
@processor_counter = Utility::ProcessorCounter.new
-
1
singleton_class.send :attr_reader, :processor_counter
-
-
1
def self.processor_count
-
processor_counter.processor_count
-
end
-
-
1
def self.physical_processor_count
-
processor_counter.physical_processor_count
-
end
-
end
-
1
module Concurrent
-
1
VERSION = '1.1.6'
-
end
-
1
require "optparse"
-
1
require "thread"
-
1
require "mutex_m"
-
1
require "minitest/parallel"
-
1
require "stringio"
-
-
##
-
# :include: README.rdoc
-
-
1
module Minitest
-
1
VERSION = "5.14.1" # :nodoc:
-
1
ENCS = "".respond_to? :encoding # :nodoc:
-
-
1
@@installed_at_exit ||= false
-
1
@@after_run = []
-
1
@extensions = []
-
-
2
mc = (class << self; self; end)
-
-
##
-
# Parallel test executor
-
-
1
mc.send :attr_accessor, :parallel_executor
-
-
1
warn "DEPRECATED: use MT_CPU instead of N for parallel test runs" if ENV["N"]
-
1
n_threads = (ENV["MT_CPU"] || ENV["N"] || 2).to_i
-
1
self.parallel_executor = Parallel::Executor.new n_threads
-
-
##
-
# Filter object for backtraces.
-
-
1
mc.send :attr_accessor, :backtrace_filter
-
-
##
-
# Reporter object to be used for all runs.
-
#
-
# NOTE: This accessor is only available during setup, not during runs.
-
-
1
mc.send :attr_accessor, :reporter
-
-
##
-
# Names of known extension plugins.
-
-
1
mc.send :attr_accessor, :extensions
-
-
##
-
# The signal to use for dumping information to STDERR. Defaults to "INFO".
-
-
1
mc.send :attr_accessor, :info_signal
-
1
self.info_signal = "INFO"
-
-
##
-
# Registers Minitest to run at process exit
-
-
1
def self.autorun
-
at_exit {
-
1
next if $! and not ($!.kind_of? SystemExit and $!.success?)
-
-
1
exit_code = nil
-
-
1
pid = Process.pid
-
1
at_exit {
-
1
next if Process.pid != pid
-
1
@@after_run.reverse_each(&:call)
-
1
exit exit_code || false
-
}
-
-
1
exit_code = Minitest.run ARGV
-
1
} unless @@installed_at_exit
-
1
@@installed_at_exit = true
-
end
-
-
##
-
# A simple hook allowing you to run a block of code after everything
-
# is done running. Eg:
-
#
-
# Minitest.after_run { p $debugging_info }
-
-
1
def self.after_run &block
-
@@after_run << block
-
end
-
-
1
def self.init_plugins options # :nodoc:
-
1
self.extensions.each do |name|
-
1
msg = "plugin_#{name}_init"
-
1
send msg, options if self.respond_to? msg
-
end
-
end
-
-
1
def self.load_plugins # :nodoc:
-
1
return unless self.extensions.empty?
-
-
1
seen = {}
-
-
1
require "rubygems" unless defined? Gem
-
-
1
Gem.find_files("minitest/*_plugin.rb").each do |plugin_path|
-
1
name = File.basename plugin_path, "_plugin.rb"
-
-
1
next if seen[name]
-
1
seen[name] = true
-
-
1
require plugin_path
-
1
self.extensions << name
-
end
-
end
-
-
##
-
# This is the top-level run method. Everything starts from here. It
-
# tells each Runnable sub-class to run, and each of those are
-
# responsible for doing whatever they do.
-
#
-
# The overall structure of a run looks like this:
-
#
-
# Minitest.autorun
-
# Minitest.run(args)
-
# Minitest.__run(reporter, options)
-
# Runnable.runnables.each
-
# runnable.run(reporter, options)
-
# self.runnable_methods.each
-
# self.run_one_method(self, runnable_method, reporter)
-
# Minitest.run_one_method(klass, runnable_method)
-
# klass.new(runnable_method).run
-
-
1
def self.run args = []
-
1
self.load_plugins unless args.delete("--no-plugins") || ENV["MT_NO_PLUGINS"]
-
-
1
options = process_args args
-
-
1
reporter = CompositeReporter.new
-
1
reporter << SummaryReporter.new(options[:io], options)
-
1
reporter << ProgressReporter.new(options[:io], options)
-
-
1
self.reporter = reporter # this makes it available to plugins
-
1
self.init_plugins options
-
1
self.reporter = nil # runnables shouldn't depend on the reporter, ever
-
-
1
self.parallel_executor.start if parallel_executor.respond_to?(:start)
-
1
reporter.start
-
begin
-
1
__run reporter, options
-
rescue Interrupt
-
warn "Interrupted. Exiting..."
-
end
-
1
self.parallel_executor.shutdown
-
1
reporter.report
-
-
1
reporter.passed?
-
end
-
-
##
-
# Internal run method. Responsible for telling all Runnable
-
# sub-classes to run.
-
-
1
def self.__run reporter, options
-
24
suites = Runnable.runnables.reject { |s| s.runnable_methods.empty? }.shuffle
-
19
parallel, serial = suites.partition { |s| s.test_order == :parallel }
-
-
# If we run the parallel tests before the serial tests, the parallel tests
-
# could run in parallel with the serial tests. This would be bad because
-
# the serial tests won't lock around Reporter#record. Run the serial tests
-
# first, so that after they complete, the parallel tests will lock when
-
# recording results.
-
19
serial.map { |suite| suite.run reporter, options } +
-
parallel.map { |suite| suite.run reporter, options }
-
end
-
-
1
def self.process_args args = [] # :nodoc:
-
options = {
-
1
:io => $stdout,
-
}
-
1
orig_args = args.dup
-
-
1
OptionParser.new do |opts|
-
1
opts.banner = "minitest options:"
-
1
opts.version = Minitest::VERSION
-
-
1
opts.on "-h", "--help", "Display this help." do
-
puts opts
-
exit
-
end
-
-
1
opts.on "--no-plugins", "Bypass minitest plugin auto-loading (or set $MT_NO_PLUGINS)."
-
-
1
desc = "Sets random seed. Also via env. Eg: SEED=n rake"
-
1
opts.on "-s", "--seed SEED", Integer, desc do |m|
-
options[:seed] = m.to_i
-
end
-
-
1
opts.on "-v", "--verbose", "Verbose. Show progress processing files." do
-
options[:verbose] = true
-
end
-
-
1
opts.on "-n", "--name PATTERN", "Filter run on /regexp/ or string." do |a|
-
options[:filter] = a
-
end
-
-
1
opts.on "-e", "--exclude PATTERN", "Exclude /regexp/ or string from run." do |a|
-
options[:exclude] = a
-
end
-
-
1
unless extensions.empty?
-
1
opts.separator ""
-
1
opts.separator "Known extensions: #{extensions.join(", ")}"
-
-
1
extensions.each do |meth|
-
1
msg = "plugin_#{meth}_options"
-
1
send msg, opts, options if self.respond_to?(msg)
-
end
-
end
-
-
begin
-
1
opts.parse! args
-
rescue OptionParser::InvalidOption => e
-
puts
-
puts e
-
puts
-
puts opts
-
exit 1
-
end
-
-
1
orig_args -= args
-
end
-
-
1
unless options[:seed] then
-
1
srand
-
1
options[:seed] = (ENV["SEED"] || srand).to_i % 0xFFFF
-
1
orig_args << "--seed" << options[:seed].to_s
-
end
-
-
1
srand options[:seed]
-
-
1
options[:args] = orig_args.map { |s|
-
2
s =~ /[\s|&<>$()]/ ? s.inspect : s
-
}.join " "
-
-
1
options
-
end
-
-
1
def self.filter_backtrace bt # :nodoc:
-
result = backtrace_filter.filter bt
-
result = bt.dup if result.empty?
-
result
-
end
-
-
##
-
# Represents anything "runnable", like Test, Spec, Benchmark, or
-
# whatever you can dream up.
-
#
-
# Subclasses of this are automatically registered and available in
-
# Runnable.runnables.
-
-
1
class Runnable
-
##
-
# Number of assertions executed in this run.
-
-
1
attr_accessor :assertions
-
-
##
-
# An assertion raised during the run, if any.
-
-
1
attr_accessor :failures
-
-
##
-
# The time it took to run.
-
-
1
attr_accessor :time
-
-
1
def time_it # :nodoc:
-
60
t0 = Minitest.clock_time
-
-
60
yield
-
ensure
-
60
self.time = Minitest.clock_time - t0
-
end
-
-
##
-
# Name of the run.
-
-
1
def name
-
180
@NAME
-
end
-
-
##
-
# Set the name of the run.
-
-
1
def name= o
-
120
@NAME = o
-
end
-
-
##
-
# Returns all instance methods matching the pattern +re+.
-
-
1
def self.methods_matching re
-
41
public_instance_methods(true).grep(re).map(&:to_s)
-
end
-
-
1
def self.reset # :nodoc:
-
1
@@runnables = []
-
end
-
-
1
reset
-
-
##
-
# Responsible for running all runnable methods in a given class,
-
# each in its own instance. Each instance is passed to the
-
# reporter to record.
-
-
1
def self.run reporter, options = {}
-
18
filter = options[:filter] || "/./"
-
18
filter = Regexp.new $1 if filter.is_a?(String) && filter =~ %r%/(.*)/%
-
-
18
filtered_methods = self.runnable_methods.find_all { |m|
-
60
filter === m || filter === "#{self}##{m}"
-
}
-
-
18
exclude = options[:exclude]
-
18
exclude = Regexp.new $1 if exclude =~ %r%/(.*)/%
-
-
18
filtered_methods.delete_if { |m|
-
60
exclude === m || exclude === "#{self}##{m}"
-
}
-
-
18
return if filtered_methods.empty?
-
-
18
with_info_handler reporter do
-
18
filtered_methods.each do |method_name|
-
60
run_one_method self, method_name, reporter
-
end
-
end
-
end
-
-
##
-
# Runs a single method and has the reporter record the result.
-
# This was considered internal API but is factored out of run so
-
# that subclasses can specialize the running of an individual
-
# test. See Minitest::ParallelTest::ClassMethods for an example.
-
-
1
def self.run_one_method klass, method_name, reporter
-
60
reporter.prerecord klass, method_name
-
60
reporter.record Minitest.run_one_method(klass, method_name)
-
end
-
-
1
def self.with_info_handler reporter, &block # :nodoc:
-
18
handler = lambda do
-
unless reporter.passed? then
-
warn "Current results:"
-
warn ""
-
warn reporter.reporters.first
-
warn ""
-
end
-
end
-
-
18
on_signal ::Minitest.info_signal, handler, &block
-
end
-
-
1
SIGNALS = Signal.list # :nodoc:
-
-
1
def self.on_signal name, action # :nodoc:
-
78
supported = SIGNALS[name]
-
-
old_trap = trap name do
-
old_trap.call if old_trap.respond_to? :call
-
action.call
-
78
end if supported
-
-
78
yield
-
ensure
-
78
trap name, old_trap if supported
-
end
-
-
##
-
# Each subclass of Runnable is responsible for overriding this
-
# method to return all runnable methods. See #methods_matching.
-
-
1
def self.runnable_methods
-
raise NotImplementedError, "subclass responsibility"
-
end
-
-
##
-
# Returns all subclasses of Runnable.
-
-
1
def self.runnables
-
24
@@runnables
-
end
-
-
1
@@marshal_dump_warned = false
-
-
1
def marshal_dump # :nodoc:
-
unless @@marshal_dump_warned then
-
warn ["Minitest::Runnable#marshal_dump is deprecated.",
-
"You might be violating internals. From", caller.first].join " "
-
@@marshal_dump_warned = true
-
end
-
-
[self.name, self.failures, self.assertions, self.time]
-
end
-
-
1
def marshal_load ary # :nodoc:
-
self.name, self.failures, self.assertions, self.time = ary
-
end
-
-
1
def failure # :nodoc:
-
180
self.failures.first
-
end
-
-
1
def initialize name # :nodoc:
-
120
self.name = name
-
120
self.failures = []
-
120
self.assertions = 0
-
end
-
-
##
-
# Runs a single method. Needs to return self.
-
-
1
def run
-
raise NotImplementedError, "subclass responsibility"
-
end
-
-
##
-
# Did this run pass?
-
#
-
# Note: skipped runs are not considered passing, but they don't
-
# cause the process to exit non-zero.
-
-
1
def passed?
-
raise NotImplementedError, "subclass responsibility"
-
end
-
-
##
-
# Returns a single character string to print based on the result
-
# of the run. One of <tt>"."</tt>, <tt>"F"</tt>,
-
# <tt>"E"</tt> or <tt>"S"</tt>.
-
-
1
def result_code
-
raise NotImplementedError, "subclass responsibility"
-
end
-
-
##
-
# Was this run skipped? See #passed? for more information.
-
-
1
def skipped?
-
raise NotImplementedError, "subclass responsibility"
-
end
-
end
-
-
##
-
# Shared code for anything that can get passed to a Reporter. See
-
# Minitest::Test & Minitest::Result.
-
-
1
module Reportable
-
##
-
# Did this run pass?
-
#
-
# Note: skipped runs are not considered passing, but they don't
-
# cause the process to exit non-zero.
-
-
1
def passed?
-
60
not self.failure
-
end
-
-
##
-
# The location identifier of this test. Depends on a method
-
# existing called class_name.
-
-
1
def location
-
loc = " [#{self.failure.location}]" unless passed? or error?
-
"#{self.class_name}##{self.name}#{loc}"
-
end
-
-
1
def class_name # :nodoc:
-
raise NotImplementedError, "subclass responsibility"
-
end
-
-
##
-
# Returns ".", "F", or "E" based on the result of the run.
-
-
1
def result_code
-
60
self.failure and self.failure.result_code or "."
-
end
-
-
##
-
# Was this run skipped?
-
-
1
def skipped?
-
60
self.failure and Skip === self.failure
-
end
-
-
##
-
# Did this run error?
-
-
1
def error?
-
self.failures.any? { |f| UnexpectedError === f }
-
end
-
end
-
-
##
-
# This represents a test result in a clean way that can be
-
# marshalled over a wire. Tests can do anything they want to the
-
# test instance and can create conditions that cause Marshal.dump to
-
# blow up. By using Result.from(a_test) you can be reasonably sure
-
# that the test result can be marshalled.
-
-
1
class Result < Runnable
-
1
include Minitest::Reportable
-
-
1
undef_method :marshal_dump
-
1
undef_method :marshal_load
-
-
##
-
# The class name of the test result.
-
-
1
attr_accessor :klass
-
-
##
-
# The location of the test method.
-
-
1
attr_accessor :source_location
-
-
##
-
# Create a new test result from a Runnable instance.
-
-
1
def self.from runnable
-
60
o = runnable
-
-
60
r = self.new o.name
-
60
r.klass = o.class.name
-
60
r.assertions = o.assertions
-
60
r.failures = o.failures.dup
-
60
r.time = o.time
-
-
60
r.source_location = o.method(o.name).source_location rescue ["unknown", -1]
-
-
60
r
-
end
-
-
1
def class_name # :nodoc:
-
self.klass # for Minitest::Reportable
-
end
-
-
1
def to_s # :nodoc:
-
return location if passed? and not skipped?
-
-
failures.map { |failure|
-
"#{failure.result_label}:\n#{self.location}:\n#{failure.message}\n"
-
}.join "\n"
-
end
-
end
-
-
##
-
# Defines the API for Reporters. Subclass this and override whatever
-
# you want. Go nuts.
-
-
1
class AbstractReporter
-
1
include Mutex_m
-
-
##
-
# Starts reporting on the run.
-
-
1
def start
-
end
-
-
##
-
# About to start running a test. This allows a reporter to show
-
# that it is starting or that we are in the middle of a test run.
-
-
1
def prerecord klass, name
-
end
-
-
##
-
# Output and record the result of the test. Call
-
# {result#result_code}[rdoc-ref:Runnable#result_code] to get the
-
# result character string. Stores the result of the run if the run
-
# did not pass.
-
-
1
def record result
-
end
-
-
##
-
# Outputs the summary of the run.
-
-
1
def report
-
end
-
-
##
-
# Did this run pass?
-
-
1
def passed?
-
1
true
-
end
-
end
-
-
1
class Reporter < AbstractReporter # :nodoc:
-
##
-
# The IO used to report.
-
-
1
attr_accessor :io
-
-
##
-
# Command-line options for this run.
-
-
1
attr_accessor :options
-
-
1
def initialize io = $stdout, options = {} # :nodoc:
-
2
super()
-
2
self.io = io
-
2
self.options = options
-
end
-
end
-
-
##
-
# A very simple reporter that prints the "dots" during the run.
-
#
-
# This is added to the top-level CompositeReporter at the start of
-
# the run. If you want to change the output of minitest via a
-
# plugin, pull this out of the composite and replace it with your
-
# own.
-
-
1
class ProgressReporter < Reporter
-
1
def prerecord klass, name #:nodoc:
-
60
if options[:verbose] then
-
io.print "%s#%s = " % [klass.name, name]
-
io.flush
-
end
-
end
-
-
1
def record result # :nodoc:
-
60
io.print "%.2f s = " % [result.time] if options[:verbose]
-
60
io.print result.result_code
-
60
io.puts if options[:verbose]
-
end
-
end
-
-
##
-
# A reporter that gathers statistics about a test run. Does not do
-
# any IO because meant to be used as a parent class for a reporter
-
# that does.
-
#
-
# If you want to create an entirely different type of output (eg,
-
# CI, HTML, etc), this is the place to start.
-
#
-
# Example:
-
#
-
# class JenkinsCIReporter < StatisticsReporter
-
# def report
-
# super # Needed to calculate some statistics
-
#
-
# print "<testsuite "
-
# print "tests='#{count}' "
-
# print "failures='#{failures}' "
-
# # Remaining XML...
-
# end
-
# end
-
-
1
class StatisticsReporter < Reporter
-
##
-
# Total number of assertions.
-
-
1
attr_accessor :assertions
-
-
##
-
# Total number of test cases.
-
-
1
attr_accessor :count
-
-
##
-
# An +Array+ of test cases that failed or were skipped.
-
-
1
attr_accessor :results
-
-
##
-
# Time the test run started. If available, the monotonic clock is
-
# used and this is a +Float+, otherwise it's an instance of
-
# +Time+.
-
-
1
attr_accessor :start_time
-
-
##
-
# Test run time. If available, the monotonic clock is used and
-
# this is a +Float+, otherwise it's an instance of +Time+.
-
-
1
attr_accessor :total_time
-
-
##
-
# Total number of tests that failed.
-
-
1
attr_accessor :failures
-
-
##
-
# Total number of tests that erred.
-
-
1
attr_accessor :errors
-
-
##
-
# Total number of tests that where skipped.
-
-
1
attr_accessor :skips
-
-
1
def initialize io = $stdout, options = {} # :nodoc:
-
1
super
-
-
1
self.assertions = 0
-
1
self.count = 0
-
1
self.results = []
-
1
self.start_time = nil
-
1
self.total_time = nil
-
1
self.failures = nil
-
1
self.errors = nil
-
1
self.skips = nil
-
end
-
-
1
def passed? # :nodoc:
-
1
results.all?(&:skipped?)
-
end
-
-
1
def start # :nodoc:
-
1
self.start_time = Minitest.clock_time
-
end
-
-
1
def record result # :nodoc:
-
60
self.count += 1
-
60
self.assertions += result.assertions
-
-
60
results << result if not result.passed? or result.skipped?
-
end
-
-
##
-
# Report on the tracked statistics.
-
-
1
def report
-
1
aggregate = results.group_by { |r| r.failure.class }
-
1
aggregate.default = [] # dumb. group_by should provide this
-
-
1
self.total_time = Minitest.clock_time - start_time
-
1
self.failures = aggregate[Assertion].size
-
1
self.errors = aggregate[UnexpectedError].size
-
1
self.skips = aggregate[Skip].size
-
end
-
end
-
-
##
-
# A reporter that prints the header, summary, and failure details at
-
# the end of the run.
-
#
-
# This is added to the top-level CompositeReporter at the start of
-
# the run. If you want to change the output of minitest via a
-
# plugin, pull this out of the composite and replace it with your
-
# own.
-
-
1
class SummaryReporter < StatisticsReporter
-
# :stopdoc:
-
1
attr_accessor :sync
-
1
attr_accessor :old_sync
-
# :startdoc:
-
-
1
def start # :nodoc:
-
1
super
-
-
1
io.puts "Run options: #{options[:args]}"
-
1
io.puts
-
1
io.puts "# Running:"
-
1
io.puts
-
-
1
self.sync = io.respond_to? :"sync=" # stupid emacs
-
1
self.old_sync, io.sync = io.sync, true if self.sync
-
end
-
-
1
def report # :nodoc:
-
1
super
-
-
1
io.sync = self.old_sync
-
-
1
io.puts unless options[:verbose] # finish the dots
-
1
io.puts
-
1
io.puts statistics
-
1
aggregated_results io
-
1
io.puts summary
-
end
-
-
1
def statistics # :nodoc:
-
1
"Finished in %.6fs, %.4f runs/s, %.4f assertions/s." %
-
[total_time, count / total_time, assertions / total_time]
-
end
-
-
1
def aggregated_results io # :nodoc:
-
1
filtered_results = results.dup
-
1
filtered_results.reject!(&:skipped?) unless options[:verbose]
-
-
1
filtered_results.each_with_index { |result, i|
-
io.puts "\n%3d) %s" % [i+1, result]
-
}
-
1
io.puts
-
1
io
-
end
-
-
1
def to_s # :nodoc:
-
aggregated_results(StringIO.new(binary_string)).string
-
end
-
-
1
def summary # :nodoc:
-
1
extra = ""
-
-
extra = "\n\nYou have skipped tests. Run with --verbose for details." if
-
1
results.any?(&:skipped?) unless options[:verbose] or ENV["MT_NO_SKIP_MSG"]
-
-
1
"%d runs, %d assertions, %d failures, %d errors, %d skips%s" %
-
[count, assertions, failures, errors, skips, extra]
-
end
-
-
1
private
-
-
1
if '<3'.respond_to? :b
-
1
def binary_string; ''.b; end
-
else
-
def binary_string; ''.force_encoding(Encoding::ASCII_8BIT); end
-
end
-
end
-
-
##
-
# Dispatch to multiple reporters as one.
-
-
1
class CompositeReporter < AbstractReporter
-
##
-
# The list of reporters to dispatch to.
-
-
1
attr_accessor :reporters
-
-
1
def initialize *reporters # :nodoc:
-
1
super()
-
1
self.reporters = reporters
-
end
-
-
1
def io # :nodoc:
-
reporters.first.io
-
end
-
-
##
-
# Add another reporter to the mix.
-
-
1
def << reporter
-
2
self.reporters << reporter
-
end
-
-
1
def passed? # :nodoc:
-
1
self.reporters.all?(&:passed?)
-
end
-
-
1
def start # :nodoc:
-
1
self.reporters.each(&:start)
-
end
-
-
1
def prerecord klass, name # :nodoc:
-
60
self.reporters.each do |reporter|
-
# TODO: remove conditional for minitest 6
-
120
reporter.prerecord klass, name if reporter.respond_to? :prerecord
-
end
-
end
-
-
1
def record result # :nodoc:
-
60
self.reporters.each do |reporter|
-
120
reporter.record result
-
end
-
end
-
-
1
def report # :nodoc:
-
1
self.reporters.each(&:report)
-
end
-
end
-
-
##
-
# Represents run failures.
-
-
1
class Assertion < Exception
-
1
def error # :nodoc:
-
self
-
end
-
-
##
-
# Where was this run before an assertion was raised?
-
-
1
def location
-
last_before_assertion = ""
-
self.backtrace.reverse_each do |s|
-
break if s =~ /in .(assert|refute|flunk|pass|fail|raise|must|wont)/
-
last_before_assertion = s
-
end
-
last_before_assertion.sub(/:in .*$/, "")
-
end
-
-
1
def result_code # :nodoc:
-
result_label[0, 1]
-
end
-
-
1
def result_label # :nodoc:
-
"Failure"
-
end
-
end
-
-
##
-
# Assertion raised when skipping a run.
-
-
1
class Skip < Assertion
-
1
def result_label # :nodoc:
-
"Skipped"
-
end
-
end
-
-
##
-
# Assertion wrapping an unexpected error that was raised during a run.
-
-
1
class UnexpectedError < Assertion
-
# TODO: figure out how to use `cause` instead
-
1
attr_accessor :error # :nodoc:
-
-
1
def initialize error # :nodoc:
-
super "Unexpected exception"
-
self.error = error
-
end
-
-
1
def backtrace # :nodoc:
-
self.error.backtrace
-
end
-
-
1
def message # :nodoc:
-
bt = Minitest.filter_backtrace(self.backtrace).join "\n "
-
"#{self.error.class}: #{self.error.message}\n #{bt}"
-
end
-
-
1
def result_label # :nodoc:
-
"Error"
-
end
-
end
-
-
##
-
# Provides a simple set of guards that you can use in your tests
-
# to skip execution if it is not applicable. These methods are
-
# mixed into Test as both instance and class methods so you
-
# can use them inside or outside of the test methods.
-
#
-
# def test_something_for_mri
-
# skip "bug 1234" if jruby?
-
# # ...
-
# end
-
#
-
# if windows? then
-
# # ... lots of test methods ...
-
# end
-
-
1
module Guard
-
-
##
-
# Is this running on jruby?
-
-
1
def jruby? platform = RUBY_PLATFORM
-
"java" == platform
-
end
-
-
##
-
# Is this running on maglev?
-
-
1
def maglev? platform = defined?(RUBY_ENGINE) && RUBY_ENGINE
-
where = Minitest.filter_backtrace(caller).first
-
where = where.split(/:in /, 2).first # clean up noise
-
warn "DEPRECATED: `maglev?` called from #{where}. This will fail in Minitest 6."
-
"maglev" == platform
-
end
-
-
##
-
# Is this running on mri?
-
-
1
def mri? platform = RUBY_DESCRIPTION
-
/^ruby/ =~ platform
-
end
-
-
##
-
# Is this running on macOS?
-
-
1
def osx? platform = RUBY_PLATFORM
-
/darwin/ =~ platform
-
end
-
-
##
-
# Is this running on rubinius?
-
-
1
def rubinius? platform = defined?(RUBY_ENGINE) && RUBY_ENGINE
-
where = Minitest.filter_backtrace(caller).first
-
where = where.split(/:in /, 2).first # clean up noise
-
warn "DEPRECATED: `rubinius?` called from #{where}. This will fail in Minitest 6."
-
"rbx" == platform
-
end
-
-
##
-
# Is this running on windows?
-
-
1
def windows? platform = RUBY_PLATFORM
-
/mswin|mingw/ =~ platform
-
end
-
end
-
-
##
-
# The standard backtrace filter for minitest.
-
#
-
# See Minitest.backtrace_filter=.
-
-
1
class BacktraceFilter
-
-
1
MT_RE = %r%lib/minitest% #:nodoc:
-
-
##
-
# Filter +bt+ to something useful. Returns the whole thing if
-
# $DEBUG (ruby) or $MT_DEBUG (env).
-
-
1
def filter bt
-
return ["No backtrace"] unless bt
-
-
return bt.dup if $DEBUG || ENV["MT_DEBUG"]
-
-
new_bt = bt.take_while { |line| line !~ MT_RE }
-
new_bt = bt.select { |line| line !~ MT_RE } if new_bt.empty?
-
new_bt = bt.dup if new_bt.empty?
-
-
new_bt
-
end
-
end
-
-
1
self.backtrace_filter = BacktraceFilter.new
-
-
1
def self.run_one_method klass, method_name # :nodoc:
-
60
result = klass.new(method_name).run
-
60
raise "#{klass}#run _must_ return a Result" unless Result === result
-
60
result
-
end
-
-
# :stopdoc:
-
-
1
if defined? Process::CLOCK_MONOTONIC # :nodoc:
-
1
def self.clock_time
-
182
Process.clock_gettime Process::CLOCK_MONOTONIC
-
end
-
else
-
def self.clock_time
-
Time.now
-
end
-
end
-
-
1
class Runnable # re-open
-
1
def self.inherited klass # :nodoc:
-
23
self.runnables << klass
-
23
super
-
end
-
end
-
-
# :startdoc:
-
end
-
-
1
require "minitest/test"
-
# encoding: UTF-8
-
-
1
require "rbconfig"
-
1
require "tempfile"
-
1
require "stringio"
-
-
1
module Minitest
-
##
-
# Minitest Assertions. All assertion methods accept a +msg+ which is
-
# printed if the assertion fails.
-
#
-
# Protocol: Nearly everything here boils up to +assert+, which
-
# expects to be able to increment an instance accessor named
-
# +assertions+. This is not provided by Assertions and must be
-
# provided by the thing including Assertions. See Minitest::Runnable
-
# for an example.
-
-
1
module Assertions
-
1
UNDEFINED = Object.new # :nodoc:
-
-
1
def UNDEFINED.inspect # :nodoc:
-
"UNDEFINED" # again with the rdoc bugs... :(
-
end
-
-
##
-
# Returns the diff command to use in #diff. Tries to intelligently
-
# figure out what diff to use.
-
-
1
def self.diff
-
return @diff if defined? @diff
-
-
@diff = if (RbConfig::CONFIG["host_os"] =~ /mswin|mingw/ &&
-
system("diff.exe", __FILE__, __FILE__)) then
-
"diff.exe -u"
-
elsif system("gdiff", __FILE__, __FILE__)
-
"gdiff -u" # solaris and kin suck
-
elsif system("diff", __FILE__, __FILE__)
-
"diff -u"
-
else
-
nil
-
end
-
end
-
-
##
-
# Set the diff command to use in #diff.
-
-
1
def self.diff= o
-
@diff = o
-
end
-
-
##
-
# Returns a diff between +exp+ and +act+. If there is no known
-
# diff command or if it doesn't make sense to diff the output
-
# (single line, short output), then it simply returns a basic
-
# comparison between the two.
-
#
-
# See +things_to_diff+ for more info.
-
-
1
def diff exp, act
-
result = nil
-
-
expect, butwas = things_to_diff(exp, act)
-
-
return "Expected: #{mu_pp exp}\n Actual: #{mu_pp act}" unless
-
expect
-
-
Tempfile.open("expect") do |a|
-
a.puts expect
-
a.flush
-
-
Tempfile.open("butwas") do |b|
-
b.puts butwas
-
b.flush
-
-
result = `#{Minitest::Assertions.diff} #{a.path} #{b.path}`
-
result.sub!(/^\-\-\- .+/, "--- expected")
-
result.sub!(/^\+\+\+ .+/, "+++ actual")
-
-
if result.empty? then
-
klass = exp.class
-
result = [
-
"No visible difference in the #{klass}#inspect output.\n",
-
"You should look at the implementation of #== on ",
-
"#{klass} or its members.\n",
-
expect,
-
].join
-
end
-
end
-
end
-
-
result
-
end
-
-
##
-
# Returns things to diff [expect, butwas], or [nil, nil] if nothing to diff.
-
#
-
# Criterion:
-
#
-
# 1. Strings include newlines or escaped newlines, but not both.
-
# 2. or: String lengths are > 30 characters.
-
# 3. or: Strings are equal to each other (but maybe different encodings?).
-
# 4. and: we found a diff executable.
-
-
1
def things_to_diff exp, act
-
expect = mu_pp_for_diff exp
-
butwas = mu_pp_for_diff act
-
-
e1, e2 = expect.include?("\n"), expect.include?("\\n")
-
b1, b2 = butwas.include?("\n"), butwas.include?("\\n")
-
-
need_to_diff =
-
(e1 ^ e2 ||
-
b1 ^ b2 ||
-
expect.size > 30 ||
-
butwas.size > 30 ||
-
expect == butwas) &&
-
Minitest::Assertions.diff
-
-
need_to_diff && [expect, butwas]
-
end
-
-
##
-
# This returns a human-readable version of +obj+. By default
-
# #inspect is called. You can override this to use #pretty_inspect
-
# if you want.
-
#
-
# See Minitest::Test.make_my_diffs_pretty!
-
-
1
def mu_pp obj
-
s = obj.inspect
-
-
if defined? Encoding then
-
s = s.encode Encoding.default_external
-
-
if String === obj && (obj.encoding != Encoding.default_external ||
-
!obj.valid_encoding?) then
-
enc = "# encoding: #{obj.encoding}"
-
val = "# valid: #{obj.valid_encoding?}"
-
s = "#{enc}\n#{val}\n#{s}"
-
end
-
end
-
-
s
-
end
-
-
##
-
# This returns a diff-able more human-readable version of +obj+.
-
# This differs from the regular mu_pp because it expands escaped
-
# newlines and makes hex-values (like object_ids) generic. This
-
# uses mu_pp to do the first pass and then cleans it up.
-
-
1
def mu_pp_for_diff obj
-
str = mu_pp obj
-
-
# both '\n' & '\\n' (_after_ mu_pp (aka inspect))
-
single = !!str.match(/(?<!\\|^)\\n/)
-
double = !!str.match(/(?<=\\|^)\\n/)
-
-
process =
-
if single ^ double then
-
if single then
-
lambda { |s| s == "\\n" ? "\n" : s } # unescape
-
else
-
lambda { |s| s == "\\\\n" ? "\\n\n" : s } # unescape a bit, add nls
-
end
-
else
-
:itself # leave it alone
-
end
-
-
str.
-
gsub(/\\?\\n/, &process).
-
gsub(/:0x[a-fA-F0-9]{4,}/m, ":0xXXXXXX") # anonymize hex values
-
end
-
-
##
-
# Fails unless +test+ is truthy.
-
-
1
def assert test, msg = nil
-
22825
self.assertions += 1
-
22825
unless test then
-
msg ||= "Expected #{mu_pp test} to be truthy."
-
msg = msg.call if Proc === msg
-
raise Minitest::Assertion, msg
-
end
-
22825
true
-
end
-
-
1
def _synchronize # :nodoc:
-
yield
-
end
-
-
##
-
# Fails unless +obj+ is empty.
-
-
1
def assert_empty obj, msg = nil
-
msg = message(msg) { "Expected #{mu_pp(obj)} to be empty" }
-
assert_respond_to obj, :empty?
-
assert obj.empty?, msg
-
end
-
-
1
E = "" # :nodoc:
-
-
##
-
# Fails unless <tt>exp == act</tt> printing the difference between
-
# the two, if possible.
-
#
-
# If there is no visible difference but the assertion fails, you
-
# should suspect that your #== is buggy, or your inspect output is
-
# missing crucial details. For nicer structural diffing, set
-
# Minitest::Test.make_my_diffs_pretty!
-
#
-
# For floats use assert_in_delta.
-
#
-
# See also: Minitest::Assertions.diff
-
-
1
def assert_equal exp, act, msg = nil
-
2163
msg = message(msg, E) { diff exp, act }
-
2163
result = assert exp == act, msg
-
-
2163
if nil == exp then
-
if Minitest::VERSION =~ /^6/ then
-
refute_nil exp, "Use assert_nil if expecting nil."
-
else
-
where = Minitest.filter_backtrace(caller).first
-
where = where.split(/:in /, 2).first # clean up noise
-
-
warn "DEPRECATED: Use assert_nil if expecting nil from #{where}. This will fail in Minitest 6."
-
end
-
end
-
-
2163
result
-
end
-
-
##
-
# For comparing Floats. Fails unless +exp+ and +act+ are within +delta+
-
# of each other.
-
#
-
# assert_in_delta Math::PI, (22.0 / 7.0), 0.01
-
-
1
def assert_in_delta exp, act, delta = 0.001, msg = nil
-
n = (exp - act).abs
-
msg = message(msg) {
-
"Expected |#{exp} - #{act}| (#{n}) to be <= #{delta}"
-
}
-
assert delta >= n, msg
-
end
-
-
##
-
# For comparing Floats. Fails unless +exp+ and +act+ have a relative
-
# error less than +epsilon+.
-
-
1
def assert_in_epsilon exp, act, epsilon = 0.001, msg = nil
-
assert_in_delta exp, act, [exp.abs, act.abs].min * epsilon, msg
-
end
-
-
##
-
# Fails unless +collection+ includes +obj+.
-
-
1
def assert_includes collection, obj, msg = nil
-
msg = message(msg) {
-
"Expected #{mu_pp(collection)} to include #{mu_pp(obj)}"
-
}
-
assert_respond_to collection, :include?
-
assert collection.include?(obj), msg
-
end
-
-
##
-
# Fails unless +obj+ is an instance of +cls+.
-
-
1
def assert_instance_of cls, obj, msg = nil
-
msg = message(msg) {
-
"Expected #{mu_pp(obj)} to be an instance of #{cls}, not #{obj.class}"
-
}
-
-
assert obj.instance_of?(cls), msg
-
end
-
-
##
-
# Fails unless +obj+ is a kind of +cls+.
-
-
1
def assert_kind_of cls, obj, msg = nil
-
msg = message(msg) {
-
"Expected #{mu_pp(obj)} to be a kind of #{cls}, not #{obj.class}" }
-
-
assert obj.kind_of?(cls), msg
-
end
-
-
##
-
# Fails unless +matcher+ <tt>=~</tt> +obj+.
-
-
1
def assert_match matcher, obj, msg = nil
-
msg = message(msg) { "Expected #{mu_pp matcher} to match #{mu_pp obj}" }
-
assert_respond_to matcher, :"=~"
-
matcher = Regexp.new Regexp.escape matcher if String === matcher
-
assert matcher =~ obj, msg
-
end
-
-
##
-
# Fails unless +obj+ is nil
-
-
1
def assert_nil obj, msg = nil
-
1
msg = message(msg) { "Expected #{mu_pp(obj)} to be nil" }
-
1
assert obj.nil?, msg
-
end
-
-
##
-
# For testing with binary operators. Eg:
-
#
-
# assert_operator 5, :<=, 4
-
-
1
def assert_operator o1, op, o2 = UNDEFINED, msg = nil
-
return assert_predicate o1, op, msg if UNDEFINED == o2
-
msg = message(msg) { "Expected #{mu_pp(o1)} to be #{op} #{mu_pp(o2)}" }
-
assert o1.__send__(op, o2), msg
-
end
-
-
##
-
# Fails if stdout or stderr do not output the expected results.
-
# Pass in nil if you don't care about that streams output. Pass in
-
# "" if you require it to be silent. Pass in a regexp if you want
-
# to pattern match.
-
#
-
# assert_output(/hey/) { method_with_output }
-
#
-
# NOTE: this uses #capture_io, not #capture_subprocess_io.
-
#
-
# See also: #assert_silent
-
-
1
def assert_output stdout = nil, stderr = nil
-
flunk "assert_output requires a block to capture output." unless
-
block_given?
-
-
out, err = capture_io do
-
yield
-
end
-
-
err_msg = Regexp === stderr ? :assert_match : :assert_equal if stderr
-
out_msg = Regexp === stdout ? :assert_match : :assert_equal if stdout
-
-
y = send err_msg, stderr, err, "In stderr" if err_msg
-
x = send out_msg, stdout, out, "In stdout" if out_msg
-
-
(!stdout || x) && (!stderr || y)
-
rescue Assertion
-
raise
-
rescue => e
-
raise UnexpectedError, e
-
end
-
-
##
-
# Fails unless +path+ exists.
-
-
1
def assert_path_exists path, msg = nil
-
msg = message(msg) { "Expected path '#{path}' to exist" }
-
assert File.exist?(path), msg
-
end
-
-
##
-
# For testing with predicates. Eg:
-
#
-
# assert_predicate str, :empty?
-
#
-
# This is really meant for specs and is front-ended by assert_operator:
-
#
-
# str.must_be :empty?
-
-
1
def assert_predicate o1, op, msg = nil
-
msg = message(msg) { "Expected #{mu_pp(o1)} to be #{op}" }
-
assert o1.__send__(op), msg
-
end
-
-
##
-
# Fails unless the block raises one of +exp+. Returns the
-
# exception matched so you can check the message, attributes, etc.
-
#
-
# +exp+ takes an optional message on the end to help explain
-
# failures and defaults to StandardError if no exception class is
-
# passed. Eg:
-
#
-
# assert_raises(CustomError) { method_with_custom_error }
-
#
-
# With custom error message:
-
#
-
# assert_raises(CustomError, 'This should have raised CustomError') { method_with_custom_error }
-
#
-
# Using the returned object:
-
#
-
# error = assert_raises(CustomError) do
-
# raise CustomError, 'This is really bad'
-
# end
-
#
-
# assert_equal 'This is really bad', error.message
-
-
1
def assert_raises *exp
-
flunk "assert_raises requires a block to capture errors." unless
-
7
block_given?
-
-
7
msg = "#{exp.pop}.\n" if String === exp.last
-
7
exp << StandardError if exp.empty?
-
-
begin
-
7
yield
-
rescue *exp => e
-
7
pass # count assertion
-
7
return e
-
rescue Minitest::Assertion # incl Skip & UnexpectedError
-
# don't count assertion
-
raise
-
rescue SignalException, SystemExit
-
raise
-
rescue Exception => e
-
flunk proc {
-
exception_details(e, "#{msg}#{mu_pp(exp)} exception expected, not")
-
}
-
end
-
-
exp = exp.first if exp.size == 1
-
-
flunk "#{msg}#{mu_pp(exp)} expected but nothing was raised."
-
end
-
-
##
-
# Fails unless +obj+ responds to +meth+.
-
-
1
def assert_respond_to obj, meth, msg = nil
-
msg = message(msg) {
-
"Expected #{mu_pp(obj)} (#{obj.class}) to respond to ##{meth}"
-
}
-
assert obj.respond_to?(meth), msg
-
end
-
-
##
-
# Fails unless +exp+ and +act+ are #equal?
-
-
1
def assert_same exp, act, msg = nil
-
msg = message(msg) {
-
data = [mu_pp(act), act.object_id, mu_pp(exp), exp.object_id]
-
"Expected %s (oid=%d) to be the same as %s (oid=%d)" % data
-
}
-
assert exp.equal?(act), msg
-
end
-
-
##
-
# +send_ary+ is a receiver, message and arguments.
-
#
-
# Fails unless the call returns a true value
-
-
1
def assert_send send_ary, m = nil
-
where = Minitest.filter_backtrace(caller).first
-
where = where.split(/:in /, 2).first # clean up noise
-
warn "DEPRECATED: assert_send. From #{where}"
-
-
recv, msg, *args = send_ary
-
m = message(m) {
-
"Expected #{mu_pp(recv)}.#{msg}(*#{mu_pp(args)}) to return true" }
-
assert recv.__send__(msg, *args), m
-
end
-
-
##
-
# Fails if the block outputs anything to stderr or stdout.
-
#
-
# See also: #assert_output
-
-
1
def assert_silent
-
assert_output "", "" do
-
yield
-
end
-
end
-
-
##
-
# Fails unless the block throws +sym+
-
-
1
def assert_throws sym, msg = nil
-
default = "Expected #{mu_pp(sym)} to have been thrown"
-
caught = true
-
catch(sym) do
-
begin
-
yield
-
rescue ThreadError => e # wtf?!? 1.8 + threads == suck
-
default += ", not \:#{e.message[/uncaught throw \`(\w+?)\'/, 1]}"
-
rescue ArgumentError => e # 1.9 exception
-
raise e unless e.message.include?("uncaught throw")
-
default += ", not #{e.message.split(/ /).last}"
-
rescue NameError => e # 1.8 exception
-
raise e unless e.name == sym
-
default += ", not #{e.name.inspect}"
-
end
-
caught = false
-
end
-
-
assert caught, message(msg) { default }
-
rescue Assertion
-
raise
-
rescue => e
-
raise UnexpectedError, e
-
end
-
-
##
-
# Captures $stdout and $stderr into strings:
-
#
-
# out, err = capture_io do
-
# puts "Some info"
-
# warn "You did a bad thing"
-
# end
-
#
-
# assert_match %r%info%, out
-
# assert_match %r%bad%, err
-
#
-
# NOTE: For efficiency, this method uses StringIO and does not
-
# capture IO for subprocesses. Use #capture_subprocess_io for
-
# that.
-
-
1
def capture_io
-
_synchronize do
-
begin
-
captured_stdout, captured_stderr = StringIO.new, StringIO.new
-
-
orig_stdout, orig_stderr = $stdout, $stderr
-
$stdout, $stderr = captured_stdout, captured_stderr
-
-
yield
-
-
return captured_stdout.string, captured_stderr.string
-
ensure
-
$stdout = orig_stdout
-
$stderr = orig_stderr
-
end
-
end
-
end
-
-
##
-
# Captures $stdout and $stderr into strings, using Tempfile to
-
# ensure that subprocess IO is captured as well.
-
#
-
# out, err = capture_subprocess_io do
-
# system "echo Some info"
-
# system "echo You did a bad thing 1>&2"
-
# end
-
#
-
# assert_match %r%info%, out
-
# assert_match %r%bad%, err
-
#
-
# NOTE: This method is approximately 10x slower than #capture_io so
-
# only use it when you need to test the output of a subprocess.
-
-
1
def capture_subprocess_io
-
_synchronize do
-
begin
-
require "tempfile"
-
-
captured_stdout, captured_stderr = Tempfile.new("out"), Tempfile.new("err")
-
-
orig_stdout, orig_stderr = $stdout.dup, $stderr.dup
-
$stdout.reopen captured_stdout
-
$stderr.reopen captured_stderr
-
-
yield
-
-
$stdout.rewind
-
$stderr.rewind
-
-
return captured_stdout.read, captured_stderr.read
-
ensure
-
captured_stdout.unlink
-
captured_stderr.unlink
-
$stdout.reopen orig_stdout
-
$stderr.reopen orig_stderr
-
-
orig_stdout.close
-
orig_stderr.close
-
captured_stdout.close
-
captured_stderr.close
-
end
-
end
-
end
-
-
##
-
# Returns details for exception +e+
-
-
1
def exception_details e, msg
-
[
-
"#{msg}",
-
"Class: <#{e.class}>",
-
"Message: <#{e.message.inspect}>",
-
"---Backtrace---",
-
"#{Minitest.filter_backtrace(e.backtrace).join("\n")}",
-
"---------------",
-
].join "\n"
-
end
-
-
##
-
# Fails after a given date (in the local time zone). This allows
-
# you to put time-bombs in your tests if you need to keep
-
# something around until a later date lest you forget about it.
-
-
1
def fail_after y,m,d,msg
-
flunk msg if Time.now > Time.local(y, m, d)
-
end
-
-
##
-
# Fails with +msg+.
-
-
1
def flunk msg = nil
-
msg ||= "Epic Fail!"
-
assert false, msg
-
end
-
-
##
-
# Returns a proc that will output +msg+ along with the default message.
-
-
1
def message msg = nil, ending = nil, &default
-
2170
proc {
-
msg = msg.call.chomp(".") if Proc === msg
-
custom_message = "#{msg}.\n" unless msg.nil? or msg.to_s.empty?
-
"#{custom_message}#{default.call}#{ending || "."}"
-
}
-
end
-
-
##
-
# used for counting assertions
-
-
1
def pass _msg = nil
-
7
assert true
-
end
-
-
##
-
# Fails if +test+ is truthy.
-
-
1
def refute test, msg = nil
-
28
msg ||= message { "Expected #{mu_pp(test)} to not be truthy" }
-
28
assert !test, msg
-
end
-
-
##
-
# Fails if +obj+ is empty.
-
-
1
def refute_empty obj, msg = nil
-
msg = message(msg) { "Expected #{mu_pp(obj)} to not be empty" }
-
assert_respond_to obj, :empty?
-
refute obj.empty?, msg
-
end
-
-
##
-
# Fails if <tt>exp == act</tt>.
-
#
-
# For floats use refute_in_delta.
-
-
1
def refute_equal exp, act, msg = nil
-
1
msg = message(msg) {
-
"Expected #{mu_pp(act)} to not be equal to #{mu_pp(exp)}"
-
}
-
1
refute exp == act, msg
-
end
-
-
##
-
# For comparing Floats. Fails if +exp+ is within +delta+ of +act+.
-
#
-
# refute_in_delta Math::PI, (22.0 / 7.0)
-
-
1
def refute_in_delta exp, act, delta = 0.001, msg = nil
-
n = (exp - act).abs
-
msg = message(msg) {
-
"Expected |#{exp} - #{act}| (#{n}) to not be <= #{delta}"
-
}
-
refute delta >= n, msg
-
end
-
-
##
-
# For comparing Floats. Fails if +exp+ and +act+ have a relative error
-
# less than +epsilon+.
-
-
1
def refute_in_epsilon a, b, epsilon = 0.001, msg = nil
-
refute_in_delta a, b, a * epsilon, msg
-
end
-
-
##
-
# Fails if +collection+ includes +obj+.
-
-
1
def refute_includes collection, obj, msg = nil
-
msg = message(msg) {
-
"Expected #{mu_pp(collection)} to not include #{mu_pp(obj)}"
-
}
-
assert_respond_to collection, :include?
-
refute collection.include?(obj), msg
-
end
-
-
##
-
# Fails if +obj+ is an instance of +cls+.
-
-
1
def refute_instance_of cls, obj, msg = nil
-
msg = message(msg) {
-
"Expected #{mu_pp(obj)} to not be an instance of #{cls}"
-
}
-
refute obj.instance_of?(cls), msg
-
end
-
-
##
-
# Fails if +obj+ is a kind of +cls+.
-
-
1
def refute_kind_of cls, obj, msg = nil
-
msg = message(msg) { "Expected #{mu_pp(obj)} to not be a kind of #{cls}" }
-
refute obj.kind_of?(cls), msg
-
end
-
-
##
-
# Fails if +matcher+ <tt>=~</tt> +obj+.
-
-
1
def refute_match matcher, obj, msg = nil
-
msg = message(msg) { "Expected #{mu_pp matcher} to not match #{mu_pp obj}" }
-
assert_respond_to matcher, :"=~"
-
matcher = Regexp.new Regexp.escape matcher if String === matcher
-
refute matcher =~ obj, msg
-
end
-
-
##
-
# Fails if +obj+ is nil.
-
-
1
def refute_nil obj, msg = nil
-
1
msg = message(msg) { "Expected #{mu_pp(obj)} to not be nil" }
-
1
refute obj.nil?, msg
-
end
-
-
##
-
# Fails if +o1+ is not +op+ +o2+. Eg:
-
#
-
# refute_operator 1, :>, 2 #=> pass
-
# refute_operator 1, :<, 2 #=> fail
-
-
1
def refute_operator o1, op, o2 = UNDEFINED, msg = nil
-
return refute_predicate o1, op, msg if UNDEFINED == o2
-
msg = message(msg) { "Expected #{mu_pp(o1)} to not be #{op} #{mu_pp(o2)}" }
-
refute o1.__send__(op, o2), msg
-
end
-
-
##
-
# Fails if +path+ exists.
-
-
1
def refute_path_exists path, msg = nil
-
msg = message(msg) { "Expected path '#{path}' to not exist" }
-
refute File.exist?(path), msg
-
end
-
-
##
-
# For testing with predicates.
-
#
-
# refute_predicate str, :empty?
-
#
-
# This is really meant for specs and is front-ended by refute_operator:
-
#
-
# str.wont_be :empty?
-
-
1
def refute_predicate o1, op, msg = nil
-
msg = message(msg) { "Expected #{mu_pp(o1)} to not be #{op}" }
-
refute o1.__send__(op), msg
-
end
-
-
##
-
# Fails if +obj+ responds to the message +meth+.
-
-
1
def refute_respond_to obj, meth, msg = nil
-
msg = message(msg) { "Expected #{mu_pp(obj)} to not respond to #{meth}" }
-
-
refute obj.respond_to?(meth), msg
-
end
-
-
##
-
# Fails if +exp+ is the same (by object identity) as +act+.
-
-
1
def refute_same exp, act, msg = nil
-
msg = message(msg) {
-
data = [mu_pp(act), act.object_id, mu_pp(exp), exp.object_id]
-
"Expected %s (oid=%d) to not be the same as %s (oid=%d)" % data
-
}
-
refute exp.equal?(act), msg
-
end
-
-
##
-
# Skips the current run. If run in verbose-mode, the skipped run
-
# gets listed at the end of the run but doesn't cause a failure
-
# exit code.
-
-
1
def skip msg = nil, bt = caller
-
msg ||= "Skipped, no message given"
-
@skip = true
-
raise Minitest::Skip, msg, bt
-
end
-
-
##
-
# Skips the current run until a given date (in the local time
-
# zone). This allows you to put some fixes on hold until a later
-
# date, but still holds you accountable and prevents you from
-
# forgetting it.
-
-
1
def skip_until y,m,d,msg
-
skip msg if Time.now < Time.local(y, m, d)
-
where = caller.first.split(/:/, 3).first(2).join ":"
-
warn "Stale skip_until %p at %s" % [msg, where]
-
end
-
-
##
-
# Was this testcase skipped? Meant for #teardown.
-
-
1
def skipped?
-
defined?(@skip) and @skip
-
end
-
end
-
end
-
begin
-
1
require "rubygems"
-
1
gem "minitest"
-
rescue Gem::LoadError
-
# do nothing
-
end
-
-
1
require "minitest"
-
1
require "minitest/spec"
-
1
require "minitest/mock"
-
1
require "minitest/hell" if ENV["MT_HELL"]
-
-
1
Minitest.autorun
-
##
-
# It's where you hide your "assertions".
-
#
-
# Please note, because of the way that expectations are implemented,
-
# all expectations (eg must_equal) are dependent upon a thread local
-
# variable +:current_spec+. If your specs rely on mixing threads into
-
# the specs themselves, you're better off using assertions or the new
-
# _(value) wrapper. For example:
-
#
-
# it "should still work in threads" do
-
# my_threaded_thingy do
-
# (1+1).must_equal 2 # bad
-
# assert_equal 2, 1+1 # good
-
# _(1 + 1).must_equal 2 # good
-
# value(1 + 1).must_equal 2 # good, also #expect
-
# _ { 1 + "1" }.must_raise TypeError # good
-
# end
-
# end
-
-
1
module Minitest::Expectations
-
-
##
-
# See Minitest::Assertions#assert_empty.
-
#
-
# _(collection).must_be_empty
-
#
-
# :method: must_be_empty
-
-
1
infect_an_assertion :assert_empty, :must_be_empty, :unary
-
-
##
-
# See Minitest::Assertions#assert_equal
-
#
-
# _(a).must_equal b
-
#
-
# :method: must_equal
-
-
1
infect_an_assertion :assert_equal, :must_equal
-
-
##
-
# See Minitest::Assertions#assert_in_delta
-
#
-
# _(n).must_be_close_to m [, delta]
-
#
-
# :method: must_be_close_to
-
-
1
infect_an_assertion :assert_in_delta, :must_be_close_to
-
-
1
infect_an_assertion :assert_in_delta, :must_be_within_delta # :nodoc:
-
-
##
-
# See Minitest::Assertions#assert_in_epsilon
-
#
-
# _(n).must_be_within_epsilon m [, epsilon]
-
#
-
# :method: must_be_within_epsilon
-
-
1
infect_an_assertion :assert_in_epsilon, :must_be_within_epsilon
-
-
##
-
# See Minitest::Assertions#assert_includes
-
#
-
# _(collection).must_include obj
-
#
-
# :method: must_include
-
-
1
infect_an_assertion :assert_includes, :must_include, :reverse
-
-
##
-
# See Minitest::Assertions#assert_instance_of
-
#
-
# _(obj).must_be_instance_of klass
-
#
-
# :method: must_be_instance_of
-
-
1
infect_an_assertion :assert_instance_of, :must_be_instance_of
-
-
##
-
# See Minitest::Assertions#assert_kind_of
-
#
-
# _(obj).must_be_kind_of mod
-
#
-
# :method: must_be_kind_of
-
-
1
infect_an_assertion :assert_kind_of, :must_be_kind_of
-
-
##
-
# See Minitest::Assertions#assert_match
-
#
-
# _(a).must_match b
-
#
-
# :method: must_match
-
-
1
infect_an_assertion :assert_match, :must_match
-
-
##
-
# See Minitest::Assertions#assert_nil
-
#
-
# _(obj).must_be_nil
-
#
-
# :method: must_be_nil
-
-
1
infect_an_assertion :assert_nil, :must_be_nil, :unary
-
-
##
-
# See Minitest::Assertions#assert_operator
-
#
-
# _(n).must_be :<=, 42
-
#
-
# This can also do predicates:
-
#
-
# _(str).must_be :empty?
-
#
-
# :method: must_be
-
-
1
infect_an_assertion :assert_operator, :must_be, :reverse
-
-
##
-
# See Minitest::Assertions#assert_output
-
#
-
# _ { ... }.must_output out_or_nil [, err]
-
#
-
# :method: must_output
-
-
1
infect_an_assertion :assert_output, :must_output, :block
-
-
##
-
# See Minitest::Assertions#assert_raises
-
#
-
# _ { ... }.must_raise exception
-
#
-
# :method: must_raise
-
-
1
infect_an_assertion :assert_raises, :must_raise, :block
-
-
##
-
# See Minitest::Assertions#assert_respond_to
-
#
-
# _(obj).must_respond_to msg
-
#
-
# :method: must_respond_to
-
-
1
infect_an_assertion :assert_respond_to, :must_respond_to, :reverse
-
-
##
-
# See Minitest::Assertions#assert_same
-
#
-
# _(a).must_be_same_as b
-
#
-
# :method: must_be_same_as
-
-
1
infect_an_assertion :assert_same, :must_be_same_as
-
-
##
-
# See Minitest::Assertions#assert_silent
-
#
-
# _ { ... }.must_be_silent
-
#
-
# :method: must_be_silent
-
-
1
infect_an_assertion :assert_silent, :must_be_silent, :block
-
-
##
-
# See Minitest::Assertions#assert_throws
-
#
-
# _ { ... }.must_throw sym
-
#
-
# :method: must_throw
-
-
1
infect_an_assertion :assert_throws, :must_throw, :block
-
-
##
-
# See Minitest::Assertions#assert_path_exists
-
#
-
# _(some_path).path_must_exist
-
#
-
# :method: path_must_exist
-
-
1
infect_an_assertion :assert_path_exists, :path_must_exist, :unary
-
-
##
-
# See Minitest::Assertions#refute_path_exists
-
#
-
# _(some_path).path_wont_exist
-
#
-
# :method: path_wont_exist
-
-
1
infect_an_assertion :refute_path_exists, :path_wont_exist, :unary
-
-
##
-
# See Minitest::Assertions#refute_empty
-
#
-
# _(collection).wont_be_empty
-
#
-
# :method: wont_be_empty
-
-
1
infect_an_assertion :refute_empty, :wont_be_empty, :unary
-
-
##
-
# See Minitest::Assertions#refute_equal
-
#
-
# _(a).wont_equal b
-
#
-
# :method: wont_equal
-
-
1
infect_an_assertion :refute_equal, :wont_equal
-
-
##
-
# See Minitest::Assertions#refute_in_delta
-
#
-
# _(n).wont_be_close_to m [, delta]
-
#
-
# :method: wont_be_close_to
-
-
1
infect_an_assertion :refute_in_delta, :wont_be_close_to
-
-
1
infect_an_assertion :refute_in_delta, :wont_be_within_delta # :nodoc:
-
-
##
-
# See Minitest::Assertions#refute_in_epsilon
-
#
-
# _(n).wont_be_within_epsilon m [, epsilon]
-
#
-
# :method: wont_be_within_epsilon
-
-
1
infect_an_assertion :refute_in_epsilon, :wont_be_within_epsilon
-
-
##
-
# See Minitest::Assertions#refute_includes
-
#
-
# _(collection).wont_include obj
-
#
-
# :method: wont_include
-
-
1
infect_an_assertion :refute_includes, :wont_include, :reverse
-
-
##
-
# See Minitest::Assertions#refute_instance_of
-
#
-
# _(obj).wont_be_instance_of klass
-
#
-
# :method: wont_be_instance_of
-
-
1
infect_an_assertion :refute_instance_of, :wont_be_instance_of
-
-
##
-
# See Minitest::Assertions#refute_kind_of
-
#
-
# _(obj).wont_be_kind_of mod
-
#
-
# :method: wont_be_kind_of
-
-
1
infect_an_assertion :refute_kind_of, :wont_be_kind_of
-
-
##
-
# See Minitest::Assertions#refute_match
-
#
-
# _(a).wont_match b
-
#
-
# :method: wont_match
-
-
1
infect_an_assertion :refute_match, :wont_match
-
-
##
-
# See Minitest::Assertions#refute_nil
-
#
-
# _(obj).wont_be_nil
-
#
-
# :method: wont_be_nil
-
-
1
infect_an_assertion :refute_nil, :wont_be_nil, :unary
-
-
##
-
# See Minitest::Assertions#refute_operator
-
#
-
# _(n).wont_be :<=, 42
-
#
-
# This can also do predicates:
-
#
-
# str.wont_be :empty?
-
#
-
# :method: wont_be
-
-
1
infect_an_assertion :refute_operator, :wont_be, :reverse
-
-
##
-
# See Minitest::Assertions#refute_respond_to
-
#
-
# _(obj).wont_respond_to msg
-
#
-
# :method: wont_respond_to
-
-
1
infect_an_assertion :refute_respond_to, :wont_respond_to, :reverse
-
-
##
-
# See Minitest::Assertions#refute_same
-
#
-
# _(a).wont_be_same_as b
-
#
-
# :method: wont_be_same_as
-
-
1
infect_an_assertion :refute_same, :wont_be_same_as
-
end
-
1
class MockExpectationError < StandardError; end # :nodoc:
-
-
1
module Minitest # :nodoc:
-
-
##
-
# A simple and clean mock object framework.
-
#
-
# All mock objects are an instance of Mock
-
-
1
class Mock
-
1
alias :__respond_to? :respond_to?
-
-
overridden_methods = %w[
-
1
===
-
class
-
inspect
-
instance_eval
-
instance_variables
-
object_id
-
public_send
-
respond_to_missing?
-
send
-
to_s
-
]
-
-
1
instance_methods.each do |m|
-
92
undef_method m unless overridden_methods.include?(m.to_s) || m =~ /^__/
-
end
-
-
1
overridden_methods.map(&:to_sym).each do |method_id|
-
10
define_method method_id do |*args, &b|
-
if @expected_calls.key? method_id then
-
method_missing(method_id, *args, &b)
-
else
-
super(*args, &b)
-
end
-
end
-
end
-
-
1
def initialize delegator = nil # :nodoc:
-
@delegator = delegator
-
@expected_calls = Hash.new { |calls, name| calls[name] = [] }
-
@actual_calls = Hash.new { |calls, name| calls[name] = [] }
-
end
-
-
##
-
# Expect that method +name+ is called, optionally with +args+ or a
-
# +blk+, and returns +retval+.
-
#
-
# @mock.expect(:meaning_of_life, 42)
-
# @mock.meaning_of_life # => 42
-
#
-
# @mock.expect(:do_something_with, true, [some_obj, true])
-
# @mock.do_something_with(some_obj, true) # => true
-
#
-
# @mock.expect(:do_something_else, true) do |a1, a2|
-
# a1 == "buggs" && a2 == :bunny
-
# end
-
#
-
# +args+ is compared to the expected args using case equality (ie, the
-
# '===' operator), allowing for less specific expectations.
-
#
-
# @mock.expect(:uses_any_string, true, [String])
-
# @mock.uses_any_string("foo") # => true
-
# @mock.verify # => true
-
#
-
# @mock.expect(:uses_one_string, true, ["foo"])
-
# @mock.uses_one_string("bar") # => raises MockExpectationError
-
#
-
# If a method will be called multiple times, specify a new expect for each one.
-
# They will be used in the order you define them.
-
#
-
# @mock.expect(:ordinal_increment, 'first')
-
# @mock.expect(:ordinal_increment, 'second')
-
#
-
# @mock.ordinal_increment # => 'first'
-
# @mock.ordinal_increment # => 'second'
-
# @mock.ordinal_increment # => raises MockExpectationError "No more expects available for :ordinal_increment"
-
#
-
-
1
def expect name, retval, args = [], &blk
-
name = name.to_sym
-
-
if block_given?
-
raise ArgumentError, "args ignored when block given" unless args.empty?
-
@expected_calls[name] << { :retval => retval, :block => blk }
-
else
-
raise ArgumentError, "args must be an array" unless Array === args
-
@expected_calls[name] << { :retval => retval, :args => args }
-
end
-
self
-
end
-
-
1
def __call name, data # :nodoc:
-
case data
-
when Hash then
-
"#{name}(#{data[:args].inspect[1..-2]}) => #{data[:retval].inspect}"
-
else
-
data.map { |d| __call name, d }.join ", "
-
end
-
end
-
-
##
-
# Verify that all methods were called as expected. Raises
-
# +MockExpectationError+ if the mock object was not called as
-
# expected.
-
-
1
def verify
-
@expected_calls.each do |name, expected|
-
actual = @actual_calls.fetch(name, nil)
-
raise MockExpectationError, "expected #{__call name, expected[0]}" unless actual
-
raise MockExpectationError, "expected #{__call name, expected[actual.size]}, got [#{__call name, actual}]" if
-
actual.size < expected.size
-
end
-
true
-
end
-
-
1
def method_missing sym, *args, &block # :nodoc:
-
unless @expected_calls.key?(sym) then
-
if @delegator && @delegator.respond_to?(sym)
-
return @delegator.public_send(sym, *args, &block)
-
else
-
raise NoMethodError, "unmocked method %p, expected one of %p" %
-
[sym, @expected_calls.keys.sort_by(&:to_s)]
-
end
-
end
-
-
index = @actual_calls[sym].length
-
expected_call = @expected_calls[sym][index]
-
-
unless expected_call then
-
raise MockExpectationError, "No more expects available for %p: %p" %
-
[sym, args]
-
end
-
-
expected_args, retval, val_block =
-
expected_call.values_at(:args, :retval, :block)
-
-
if val_block then
-
# keep "verify" happy
-
@actual_calls[sym] << expected_call
-
-
raise MockExpectationError, "mocked method %p failed block w/ %p" %
-
[sym, args] unless val_block.call(*args, &block)
-
-
return retval
-
end
-
-
if expected_args.size != args.size then
-
raise ArgumentError, "mocked method %p expects %d arguments, got %d" %
-
[sym, expected_args.size, args.size]
-
end
-
-
zipped_args = expected_args.zip(args)
-
fully_matched = zipped_args.all? { |mod, a|
-
mod === a or mod == a
-
}
-
-
unless fully_matched then
-
raise MockExpectationError, "mocked method %p called with unexpected arguments %p" %
-
[sym, args]
-
end
-
-
@actual_calls[sym] << {
-
:retval => retval,
-
:args => zipped_args.map! { |mod, a| mod === a ? mod : a },
-
}
-
-
retval
-
end
-
-
1
def respond_to? sym, include_private = false # :nodoc:
-
return true if @expected_calls.key? sym.to_sym
-
return true if @delegator && @delegator.respond_to?(sym, include_private)
-
__respond_to?(sym, include_private)
-
end
-
end
-
end
-
-
1
module Minitest::Assertions
-
##
-
# Assert that the mock verifies correctly.
-
-
1
def assert_mock mock
-
assert mock.verify
-
end
-
end
-
-
##
-
# Object extensions for Minitest::Mock.
-
-
1
class Object
-
-
##
-
# Add a temporary stubbed method replacing +name+ for the duration
-
# of the +block+. If +val_or_callable+ responds to #call, then it
-
# returns the result of calling it, otherwise returns the value
-
# as-is. If stubbed method yields a block, +block_args+ will be
-
# passed along. Cleans up the stub at the end of the +block+. The
-
# method +name+ must exist before stubbing.
-
#
-
# def test_stale_eh
-
# obj_under_test = Something.new
-
# refute obj_under_test.stale?
-
#
-
# Time.stub :now, Time.at(0) do
-
# assert obj_under_test.stale?
-
# end
-
# end
-
#
-
-
1
def stub name, val_or_callable, *block_args
-
new_name = "__minitest_stub__#{name}"
-
-
metaclass = class << self; self; end
-
-
if respond_to? name and not methods.map(&:to_s).include? name.to_s then
-
metaclass.send :define_method, name do |*args|
-
super(*args)
-
end
-
end
-
-
metaclass.send :alias_method, new_name, name
-
-
metaclass.send :define_method, name do |*args, &blk|
-
if val_or_callable.respond_to? :call then
-
val_or_callable.call(*args, &blk)
-
else
-
blk.call(*block_args) if blk
-
val_or_callable
-
end
-
end
-
-
yield self
-
ensure
-
metaclass.send :undef_method, name
-
metaclass.send :alias_method, name, new_name
-
metaclass.send :undef_method, new_name
-
end
-
end
-
1
module Minitest
-
1
module Parallel #:nodoc:
-
-
##
-
# The engine used to run multiple tests in parallel.
-
-
1
class Executor
-
-
##
-
# The size of the pool of workers.
-
-
1
attr_reader :size
-
-
##
-
# Create a parallel test executor of with +size+ workers.
-
-
1
def initialize size
-
1
@size = size
-
1
@queue = Queue.new
-
1
@pool = nil
-
end
-
-
##
-
# Start the executor
-
-
1
def start
-
1
@pool = size.times.map {
-
2
Thread.new(@queue) do |queue|
-
2
Thread.current.abort_on_exception = true
-
4
while (job = queue.pop)
-
klass, method, reporter = job
-
reporter.synchronize { reporter.prerecord klass, method }
-
result = Minitest.run_one_method klass, method
-
reporter.synchronize { reporter.record result }
-
end
-
end
-
}
-
end
-
-
##
-
# Add a job to the queue
-
-
1
def << work; @queue << work; end
-
-
##
-
# Shuts down the pool of workers by signalling them to quit and
-
# waiting for them all to finish what they're currently working
-
# on.
-
-
1
def shutdown
-
3
size.times { @queue << nil }
-
1
@pool.each(&:join)
-
end
-
end
-
-
1
module Test # :nodoc:
-
1
def _synchronize; Minitest::Test.io_lock.synchronize { yield }; end # :nodoc:
-
-
1
module ClassMethods # :nodoc:
-
1
def run_one_method klass, method_name, reporter
-
Minitest.parallel_executor << [klass, method_name, reporter]
-
end
-
-
1
def test_order
-
:parallel
-
end
-
end
-
end
-
end
-
end
-
1
require "minitest"
-
-
1
module Minitest
-
1
def self.plugin_pride_options opts, _options # :nodoc:
-
1
opts.on "-p", "--pride", "Pride. Show your testing pride!" do
-
PrideIO.pride!
-
end
-
end
-
-
1
def self.plugin_pride_init options # :nodoc:
-
1
if PrideIO.pride? then
-
klass = ENV["TERM"] =~ /^xterm|-256color$/ ? PrideLOL : PrideIO
-
io = klass.new options[:io]
-
-
self.reporter.reporters.grep(Minitest::Reporter).each do |rep|
-
rep.io = io if rep.io.tty?
-
end
-
end
-
end
-
-
##
-
# Show your testing pride!
-
-
1
class PrideIO
-
##
-
# Activate the pride plugin. Called from both -p option and minitest/pride
-
-
1
def self.pride!
-
@pride = true
-
end
-
-
##
-
# Are we showing our testing pride?
-
-
1
def self.pride?
-
1
@pride ||= false
-
end
-
-
# Start an escape sequence
-
1
ESC = "\e["
-
-
# End the escape sequence
-
1
NND = "#{ESC}0m"
-
-
# The IO we're going to pipe through.
-
1
attr_reader :io
-
-
1
def initialize io # :nodoc:
-
@io = io
-
# stolen from /System/Library/Perl/5.10.0/Term/ANSIColor.pm
-
# also reference http://en.wikipedia.org/wiki/ANSI_escape_code
-
@colors ||= (31..36).to_a
-
@size = @colors.size
-
@index = 0
-
end
-
-
##
-
# Wrap print to colorize the output.
-
-
1
def print o
-
case o
-
when "." then
-
io.print pride o
-
when "E", "F" then
-
io.print "#{ESC}41m#{ESC}37m#{o}#{NND}"
-
when "S" then
-
io.print pride o
-
else
-
io.print o
-
end
-
end
-
-
1
def puts *o # :nodoc:
-
o.map! { |s|
-
s.to_s.sub(/Finished/) {
-
@index = 0
-
"Fabulous run".split(//).map { |c|
-
pride(c)
-
}.join
-
}
-
}
-
-
io.puts(*o)
-
end
-
-
##
-
# Color a string.
-
-
1
def pride string
-
string = "*" if string == "."
-
c = @colors[@index % @size]
-
@index += 1
-
"#{ESC}#{c}m#{string}#{NND}"
-
end
-
-
1
def method_missing msg, *args # :nodoc:
-
io.send(msg, *args)
-
end
-
end
-
-
##
-
# If you thought the PrideIO was colorful...
-
#
-
# (Inspired by lolcat, but with clean math)
-
-
1
class PrideLOL < PrideIO
-
1
PI_3 = Math::PI / 3 # :nodoc:
-
-
1
def initialize io # :nodoc:
-
# walk red, green, and blue around a circle separated by equal thirds.
-
#
-
# To visualize, type this into wolfram-alpha:
-
#
-
# plot (3*sin(x)+3), (3*sin(x+2*pi/3)+3), (3*sin(x+4*pi/3)+3)
-
-
# 6 has wide pretty gradients. 3 == lolcat, about half the width
-
@colors = (0...(6 * 7)).map { |n|
-
n *= 1.0 / 6
-
r = (3 * Math.sin(n ) + 3).to_i
-
g = (3 * Math.sin(n + 2 * PI_3) + 3).to_i
-
b = (3 * Math.sin(n + 4 * PI_3) + 3).to_i
-
-
# Then we take rgb and encode them in a single number using base 6.
-
# For some mysterious reason, we add 16... to clear the bottom 4 bits?
-
# Yes... they're ugly.
-
-
36 * r + 6 * g + b + 16
-
}
-
-
super
-
end
-
-
##
-
# Make the string even more colorful. Damnit.
-
-
1
def pride string
-
c = @colors[@index % @size]
-
@index += 1
-
"#{ESC}38;5;#{c}m#{string}#{NND}"
-
end
-
end
-
end
-
1
require "minitest/test"
-
-
1
class Module # :nodoc:
-
1
def infect_an_assertion meth, new_name, dont_flip = false # :nodoc:
-
32
block = dont_flip == :block
-
32
dont_flip = false if block
-
-
# warn "%-22p -> %p %p" % [meth, new_name, dont_flip]
-
32
self.class_eval <<-EOM, __FILE__, __LINE__ + 1
-
def #{new_name} *args
-
where = Minitest.filter_backtrace(caller).first
-
where = where.split(/:in /, 2).first # clean up noise
-
warn "DEPRECATED: global use of #{new_name} from #\{where}. Use _(obj).#{new_name} instead. This will fail in Minitest 6."
-
Minitest::Expectation.new(self, Minitest::Spec.current).#{new_name}(*args)
-
end
-
EOM
-
-
32
Minitest::Expectation.class_eval <<-EOM, __FILE__, __LINE__ + 1
-
def #{new_name} *args
-
raise "Calling ##{new_name} outside of test." unless ctx
-
case
-
when #{!!dont_flip} then
-
ctx.#{meth}(target, *args)
-
when #{block} && Proc === target then
-
ctx.#{meth}(*args, &target)
-
else
-
ctx.#{meth}(args.first, target, *args[1..-1])
-
end
-
end
-
EOM
-
end
-
end
-
-
1
Minitest::Expectation = Struct.new :target, :ctx # :nodoc:
-
-
##
-
# Kernel extensions for minitest
-
-
1
module Kernel
-
##
-
# Describe a series of expectations for a given target +desc+.
-
#
-
# Defines a test class subclassing from either Minitest::Spec or
-
# from the surrounding describe's class. The surrounding class may
-
# subclass Minitest::Spec manually in order to easily share code:
-
#
-
# class MySpec < Minitest::Spec
-
# # ... shared code ...
-
# end
-
#
-
# class TestStuff < MySpec
-
# it "does stuff" do
-
# # shared code available here
-
# end
-
# describe "inner stuff" do
-
# it "still does stuff" do
-
# # ...and here
-
# end
-
# end
-
# end
-
#
-
# For more information on getting started with writing specs, see:
-
#
-
# http://www.rubyinside.com/a-minitestspec-tutorial-elegant-spec-style-testing-that-comes-with-ruby-5354.html
-
#
-
# For some suggestions on how to improve your specs, try:
-
#
-
# http://betterspecs.org
-
#
-
# but do note that several items there are debatable or specific to
-
# rspec.
-
#
-
# For more information about expectations, see Minitest::Expectations.
-
-
1
def describe desc, *additional_desc, &block # :doc:
-
stack = Minitest::Spec.describe_stack
-
name = [stack.last, desc, *additional_desc].compact.join("::")
-
sclas = stack.last || if Class === self && kind_of?(Minitest::Spec::DSL) then
-
self
-
else
-
Minitest::Spec.spec_type desc, *additional_desc
-
end
-
-
cls = sclas.create name, desc
-
-
stack.push cls
-
cls.class_eval(&block)
-
stack.pop
-
cls
-
end
-
1
private :describe
-
end
-
-
##
-
# Minitest::Spec -- The faster, better, less-magical spec framework!
-
#
-
# For a list of expectations, see Minitest::Expectations.
-
-
1
class Minitest::Spec < Minitest::Test
-
-
1
def self.current # :nodoc:
-
Thread.current[:current_spec]
-
end
-
-
1
def initialize name # :nodoc:
-
super
-
Thread.current[:current_spec] = self
-
end
-
-
##
-
# Oh look! A Minitest::Spec::DSL module! Eat your heart out DHH.
-
-
1
module DSL
-
##
-
# Contains pairs of matchers and Spec classes to be used to
-
# calculate the superclass of a top-level describe. This allows for
-
# automatically customizable spec types.
-
#
-
# See: register_spec_type and spec_type
-
-
1
TYPES = [[//, Minitest::Spec]]
-
-
##
-
# Register a new type of spec that matches the spec's description.
-
# This method can take either a Regexp and a spec class or a spec
-
# class and a block that takes the description and returns true if
-
# it matches.
-
#
-
# Eg:
-
#
-
# register_spec_type(/Controller$/, Minitest::Spec::Rails)
-
#
-
# or:
-
#
-
# register_spec_type(Minitest::Spec::RailsModel) do |desc|
-
# desc.superclass == ActiveRecord::Base
-
# end
-
-
1
def register_spec_type *args, &block
-
if block then
-
matcher, klass = block, args.first
-
else
-
matcher, klass = *args
-
end
-
TYPES.unshift [matcher, klass]
-
end
-
-
##
-
# Figure out the spec class to use based on a spec's description. Eg:
-
#
-
# spec_type("BlahController") # => Minitest::Spec::Rails
-
-
1
def spec_type desc, *additional
-
TYPES.find { |matcher, _klass|
-
if matcher.respond_to? :call then
-
matcher.call desc, *additional
-
else
-
matcher === desc.to_s
-
end
-
}.last
-
end
-
-
1
def describe_stack # :nodoc:
-
Thread.current[:describe_stack] ||= []
-
end
-
-
1
def children # :nodoc:
-
@children ||= []
-
end
-
-
1
def nuke_test_methods! # :nodoc:
-
self.public_instance_methods.grep(/^test_/).each do |name|
-
self.send :undef_method, name
-
end
-
end
-
-
##
-
# Define a 'before' action. Inherits the way normal methods should.
-
#
-
# NOTE: +type+ is ignored and is only there to make porting easier.
-
#
-
# Equivalent to Minitest::Test#setup.
-
-
1
def before _type = nil, &block
-
define_method :setup do
-
super()
-
self.instance_eval(&block)
-
end
-
end
-
-
##
-
# Define an 'after' action. Inherits the way normal methods should.
-
#
-
# NOTE: +type+ is ignored and is only there to make porting easier.
-
#
-
# Equivalent to Minitest::Test#teardown.
-
-
1
def after _type = nil, &block
-
define_method :teardown do
-
self.instance_eval(&block)
-
super()
-
end
-
end
-
-
##
-
# Define an expectation with name +desc+. Name gets morphed to a
-
# proper test method name. For some freakish reason, people who
-
# write specs don't like class inheritance, so this goes way out of
-
# its way to make sure that expectations aren't inherited.
-
#
-
# This is also aliased to #specify and doesn't require a +desc+ arg.
-
#
-
# Hint: If you _do_ want inheritance, use minitest/test. You can mix
-
# and match between assertions and expectations as much as you want.
-
-
1
def it desc = "anonymous", &block
-
block ||= proc { skip "(no tests defined)" }
-
-
@specs ||= 0
-
@specs += 1
-
-
name = "test_%04d_%s" % [ @specs, desc ]
-
-
undef_klasses = self.children.reject { |c| c.public_method_defined? name }
-
-
define_method name, &block
-
-
undef_klasses.each do |undef_klass|
-
undef_klass.send :undef_method, name
-
end
-
-
name
-
end
-
-
##
-
# Essentially, define an accessor for +name+ with +block+.
-
#
-
# Why use let instead of def? I honestly don't know.
-
-
1
def let name, &block
-
name = name.to_s
-
pre, post = "let '#{name}' cannot ", ". Please use another name."
-
methods = Minitest::Spec.instance_methods.map(&:to_s) - %w[subject]
-
raise ArgumentError, "#{pre}begin with 'test'#{post}" if
-
name =~ /\Atest/
-
raise ArgumentError, "#{pre}override a method in Minitest::Spec#{post}" if
-
methods.include? name
-
-
define_method name do
-
@_memoized ||= {}
-
@_memoized.fetch(name) { |k| @_memoized[k] = instance_eval(&block) }
-
end
-
end
-
-
##
-
# Another lazy man's accessor generator. Made even more lazy by
-
# setting the name for you to +subject+.
-
-
1
def subject &block
-
let :subject, &block
-
end
-
-
1
def create name, desc # :nodoc:
-
cls = Class.new(self) do
-
@name = name
-
@desc = desc
-
-
nuke_test_methods!
-
end
-
-
children << cls
-
-
cls
-
end
-
-
1
def name # :nodoc:
-
defined?(@name) ? @name : super
-
end
-
-
1
def to_s # :nodoc:
-
name # Can't alias due to 1.8.7, not sure why
-
end
-
-
1
attr_reader :desc # :nodoc:
-
1
alias :specify :it
-
-
##
-
# Rdoc... why are you so dumb?
-
-
1
module InstanceMethods
-
##
-
# Takes a value or a block and returns a value monad that has
-
# all of Expectations methods available to it.
-
#
-
# _(1 + 1).must_equal 2
-
#
-
# And for blocks:
-
#
-
# _ { 1 + "1" }.must_raise TypeError
-
#
-
# This method of expectation-based testing is preferable to
-
# straight-expectation methods (on Object) because it stores its
-
# test context, bypassing our hacky use of thread-local variables.
-
#
-
# NOTE: At some point, the methods on Object will be deprecated
-
# and then removed.
-
#
-
# It is also aliased to #value and #expect for your aesthetic
-
# pleasure:
-
#
-
# _(1 + 1).must_equal 2
-
# value(1 + 1).must_equal 2
-
# expect(1 + 1).must_equal 2
-
-
1
def _ value = nil, &block
-
Minitest::Expectation.new block || value, self
-
end
-
-
1
alias value _
-
1
alias expect _
-
-
1
def before_setup # :nodoc:
-
super
-
Thread.current[:current_spec] = self
-
end
-
end
-
-
1
def self.extended obj # :nodoc:
-
1
obj.send :include, InstanceMethods
-
end
-
end
-
-
1
extend DSL
-
-
1
TYPES = DSL::TYPES # :nodoc:
-
end
-
-
1
require "minitest/expectations"
-
-
1
class Object # :nodoc:
-
1
include Minitest::Expectations unless ENV["MT_NO_EXPECTATIONS"]
-
end
-
1
require "minitest" unless defined? Minitest::Runnable
-
-
1
module Minitest
-
##
-
# Subclass Test to create your own tests. Typically you'll want a
-
# Test subclass per implementation class.
-
#
-
# See Minitest::Assertions
-
-
1
class Test < Runnable
-
1
require "minitest/assertions"
-
1
include Minitest::Assertions
-
1
include Minitest::Reportable
-
-
1
def class_name # :nodoc:
-
self.class.name # for Minitest::Reportable
-
end
-
-
1
PASSTHROUGH_EXCEPTIONS = [NoMemoryError, SignalException, SystemExit] # :nodoc:
-
-
# :stopdoc:
-
2
class << self; attr_accessor :io_lock; end
-
1
self.io_lock = Mutex.new
-
# :startdoc:
-
-
##
-
# Call this at the top of your tests when you absolutely
-
# positively need to have ordered tests. In doing so, you're
-
# admitting that you suck and your tests are weak.
-
-
1
def self.i_suck_and_my_tests_are_order_dependent!
-
class << self
-
undef_method :test_order if method_defined? :test_order
-
define_method :test_order do :alpha end
-
end
-
end
-
-
##
-
# Make diffs for this Test use #pretty_inspect so that diff
-
# in assert_equal can have more details. NOTE: this is much slower
-
# than the regular inspect but much more usable for complex
-
# objects.
-
-
1
def self.make_my_diffs_pretty!
-
require "pp"
-
-
define_method :mu_pp, &:pretty_inspect
-
end
-
-
##
-
# Call this at the top of your tests when you want to run your
-
# tests in parallel. In doing so, you're admitting that you rule
-
# and your tests are awesome.
-
-
1
def self.parallelize_me!
-
include Minitest::Parallel::Test
-
extend Minitest::Parallel::Test::ClassMethods
-
end
-
-
##
-
# Returns all instance methods starting with "test_". Based on
-
# #test_order, the methods are either sorted, randomized
-
# (default), or run in parallel.
-
-
1
def self.runnable_methods
-
41
methods = methods_matching(/^test_/)
-
-
41
case self.test_order
-
when :random, :parallel then
-
41
max = methods.size
-
161
methods.sort.sort_by { rand max }
-
when :alpha, :sorted then
-
methods.sort
-
else
-
raise "Unknown test_order: #{self.test_order.inspect}"
-
end
-
end
-
-
##
-
# Defines the order to run tests (:random by default). Override
-
# this or use a convenience method to change it for your tests.
-
-
1
def self.test_order
-
59
:random
-
end
-
-
1
TEARDOWN_METHODS = %w[ before_teardown teardown after_teardown ] # :nodoc:
-
-
##
-
# Runs a single test with setup/teardown hooks.
-
-
1
def run
-
60
with_info_handler do
-
60
time_it do
-
60
capture_exceptions do
-
60
before_setup; setup; after_setup
-
-
60
self.send self.name
-
end
-
-
60
TEARDOWN_METHODS.each do |hook|
-
180
capture_exceptions do
-
180
self.send hook
-
end
-
end
-
end
-
end
-
-
60
Result.from self # per contract
-
end
-
-
##
-
# Provides before/after hooks for setup and teardown. These are
-
# meant for library writers, NOT for regular test authors. See
-
# #before_setup for an example.
-
-
1
module LifecycleHooks
-
-
##
-
# Runs before every test, before setup. This hook is meant for
-
# libraries to extend minitest. It is not meant to be used by
-
# test developers.
-
#
-
# As a simplistic example:
-
#
-
# module MyMinitestPlugin
-
# def before_setup
-
# super
-
# # ... stuff to do before setup is run
-
# end
-
#
-
# def after_setup
-
# # ... stuff to do after setup is run
-
# super
-
# end
-
#
-
# def before_teardown
-
# super
-
# # ... stuff to do before teardown is run
-
# end
-
#
-
# def after_teardown
-
# # ... stuff to do after teardown is run
-
# super
-
# end
-
# end
-
#
-
# class MiniTest::Test
-
# include MyMinitestPlugin
-
# end
-
-
1
def before_setup; end
-
-
##
-
# Runs before every test. Use this to set up before each test
-
# run.
-
-
1
def setup; end
-
-
##
-
# Runs before every test, after setup. This hook is meant for
-
# libraries to extend minitest. It is not meant to be used by
-
# test developers.
-
#
-
# See #before_setup for an example.
-
-
1
def after_setup; end
-
-
##
-
# Runs after every test, before teardown. This hook is meant for
-
# libraries to extend minitest. It is not meant to be used by
-
# test developers.
-
#
-
# See #before_setup for an example.
-
-
1
def before_teardown; end
-
-
##
-
# Runs after every test. Use this to clean up after each test
-
# run.
-
-
1
def teardown; end
-
-
##
-
# Runs after every test, after teardown. This hook is meant for
-
# libraries to extend minitest. It is not meant to be used by
-
# test developers.
-
#
-
# See #before_setup for an example.
-
-
1
def after_teardown; end
-
end # LifecycleHooks
-
-
1
def capture_exceptions # :nodoc:
-
240
yield
-
rescue *PASSTHROUGH_EXCEPTIONS
-
raise
-
rescue Assertion => e
-
self.failures << e
-
rescue Exception => e
-
self.failures << UnexpectedError.new(e)
-
end
-
-
1
def with_info_handler &block # :nodoc:
-
60
t0 = Minitest.clock_time
-
-
60
handler = lambda do
-
warn "\nCurrent: %s#%s %.2fs" % [self.class, self.name, Minitest.clock_time - t0]
-
end
-
-
60
self.class.on_signal ::Minitest.info_signal, handler, &block
-
end
-
-
1
include LifecycleHooks
-
1
include Guard
-
1
extend Guard
-
end # Test
-
end
-
-
1
require "minitest/unit" unless defined?(MiniTest) # compatibility layer only
-
# :stopdoc:
-
-
1
unless defined?(Minitest) then
-
# all of this crap is just to avoid circular requires and is only
-
# needed if a user requires "minitest/unit" directly instead of
-
# "minitest/autorun", so we also warn
-
-
from = caller.reject { |s| s =~ /rubygems/ }.join("\n ")
-
warn "Warning: you should require 'minitest/autorun' instead."
-
warn %(Warning: or add 'gem "minitest"' before 'require "minitest/autorun"')
-
warn "From:\n #{from}"
-
-
module Minitest; end
-
MiniTest = Minitest # prevents minitest.rb from requiring back to us
-
require "minitest"
-
end
-
-
1
MiniTest = Minitest unless defined?(MiniTest)
-
-
1
module Minitest
-
1
class Unit
-
1
VERSION = Minitest::VERSION
-
1
class TestCase < Minitest::Test
-
1
def self.inherited klass # :nodoc:
-
from = caller.first
-
warn "MiniTest::Unit::TestCase is now Minitest::Test. From #{from}"
-
super
-
end
-
end
-
-
1
def self.autorun # :nodoc:
-
from = caller.first
-
warn "MiniTest::Unit.autorun is now Minitest.autorun. From #{from}"
-
Minitest.autorun
-
end
-
-
1
def self.after_tests &b # :nodoc:
-
from = caller.first
-
warn "MiniTest::Unit.after_tests is now Minitest.after_run. From #{from}"
-
Minitest.after_run(&b)
-
end
-
end
-
end
-
-
# :startdoc: